Repository: VictoriaMetrics/VictoriaMetrics
Pull request #2673 (2022-06-01 19:44:26)

lib/storage: limit max mergeConcurrency value for systems with high number of CPUs
Workers count for merges affects the max part size during merges. Such behaviour
protects storage from running out of disk space for scenario when all workers
are merging parts with the max size.

This works very well for most cases. But for systems where high number of CPUs
is allocated for vmstorage components this could significantly impact the max
part size and result in more unmerged parts than expected.

While checking multiple production highly loaded setups it was discovered that
`max_over_time(vm_active_merges{type="storage/big}[1h]}"` rarely exceeds 2,
and `max_over_time(vm_active_merges{type="storage/small}[1h]}"` rarely exceeds 4.
The change in this commit limits the max value for concurrency accordingly.

Signed-off-by: hagen1778 <roman@victoriametrics.com>


Patch:
diff --git a/README.md b/README.md
index 802405aba..0800119aa 100644
--- a/README.md
+++ b/README.md
@@ -17,10 +17,10 @@ VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMet
 and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). 
 Just download VictoriaMetrics and follow [these instructions](https://docs.victoriametrics.com/Quick-Start.html).
 
-Cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+The cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
 
-Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow 
-[QuickStart guide](https://docs.victoriametrics.com/Quick-Start.html) for better experience.
+Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow the 
+[QuickStart guide](https://docs.victoriametrics.com/Quick-Start.html) for a better experience.
 
 [Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. 
 See [features available in enterprise package](https://victoriametrics.com/products/enterprise/).
@@ -32,8 +32,8 @@ from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/rele
 VictoriaMetrics has the following prominent features:
 
 * It can be used as long-term storage for Prometheus. See [these docs](#prometheus-setup) for details.
-* It can be used as drop-in replacement for Prometheus in Grafana, because it supports [Prometheus querying API](#prometheus-querying-api-usage).
-* It can be used as drop-in replacement for Graphite in Grafana, because it supports [Graphite API](#graphite-api-usage).
+* It can be used as a drop-in replacement for Prometheus in Grafana, because it supports [Prometheus querying API](#prometheus-querying-api-usage).
+* It can be used as  a drop-in replacement for Graphite in Grafana, because it supports [Graphite API](#graphite-api-usage).
 * It features easy setup and operation:
   * VictoriaMetrics consists of a single [small executable](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d) without external dependencies.
   * All the configuration is done via explicit command-line flags with reasonable defaults.
@@ -824,6 +824,11 @@ Each JSON line contains samples for a single time series. An example output:
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 Optional `max_rows_per_line` arg may be added to the request for limiting the maximum number of rows exported per each JSON line.
 Optional `reduce_mem_usage=1` arg may be added to the request for reducing memory usage when exporting big number of time series.
@@ -863,6 +868,11 @@ for metrics to export.
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export/csv -d 'format=<format>' -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export/csv -d 'format=<format>' -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 The exported CSV data can be imported to VictoriaMetrics via [/api/v1/import/csv](#how-to-import-csv-data).
 
@@ -885,6 +895,11 @@ wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export/native -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export/native -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 The exported data can be imported to VictoriaMetrics via [/api/v1/import/native](#how-to-import-data-in-native-format).
 The native export format may change in incompatible way between VictoriaMetrics releases, so the data exported from the release X
@@ -1079,8 +1094,13 @@ VictoriaMetrics exports [Prometheus-compatible federation data](https://promethe
 at `http://<victoriametrics-addr>:8428/federate?match[]=<timeseries_selector_for_federation>`.
 
 Optional `start` and `end` args may be added to the request in order to scrape the last point for each selected time series on the `[start ... end]` interval.
-`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values. By default, the last point
-on the interval `[now - max_lookback ... now]` is scraped for each time series. The default value for `max_lookback` is `5m` (5 minutes), but it can be overridden.
+`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/federate -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/federate -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
+By default, the last point on the interval `[now - max_lookback ... now]` is scraped for each time series. The default value for `max_lookback` is `5m` (5 minutes), but it can be overridden with `max_lookback` query arg.
 For instance, `/federate?match[]=up&max_lookback=1h` would return last points on the `[now - 1h ... now]` interval. This may be useful for time series federation
 with scrape intervals exceeding `5m`.
 
@@ -1259,7 +1279,7 @@ The downsampling can be evaluated for free by downloading and using enterprise b
 
 ## Multi-tenancy
 
-Single-node VictoriaMetrics doesn't support multi-tenancy. Use [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy) instead.
+Single-node VictoriaMetrics doesn't support multi-tenancy. Use the [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy) instead.
 
 ## Scalability and cluster version
 
@@ -1267,7 +1287,7 @@ Though single-node VictoriaMetrics cannot scale to multiple nodes, it is optimiz
 This means that a single-node VictoriaMetrics may scale vertically and substitute a moderately sized cluster built with competing solutions
 such as Thanos, Uber M3, InfluxDB or TimescaleDB. See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
 
-So try single-node VictoriaMetrics at first and then [switch to cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
+So try single-node VictoriaMetrics at first and then [switch to the cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
 horizontally scalable long-term remote storage for really large Prometheus deployments.
 [Contact us](mailto:info@victoriametrics.com) for enterprise support.
 
@@ -1342,7 +1362,7 @@ The most interesting metrics are:
   aka [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
 * `increase(vm_new_timeseries_created_total[1h])` - time series [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) during the previous hour.
 * `sum(vm_rows{type=~"storage/.*"})` - total number of `(timestamp, value)` data points in the database.
-* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted int the database per second.
+* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted in the database per second.
 * `vm_free_disk_space_bytes` - free space left at `-storageDataPath`.
 * `sum(vm_data_size_bytes)` - the total size of data on disk.
 * `increase(vm_slow_row_inserts_total[5m])` - the number of slow inserts during the last 5 minutes.
diff --git a/app/victoria-metrics/main.go b/app/victoria-metrics/main.go
index 44e90005d..4d5e3925d 100644
--- a/app/victoria-metrics/main.go
+++ b/app/victoria-metrics/main.go
@@ -92,7 +92,8 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
 		fmt.Fprintf(w, "Useful endpoints:</br>")
 		httpserver.WriteAPIHelp(w, [][2]string{
 			{"vmui", "Web UI"},
-			{"targets", "discovered targets list"},
+			{"targets", "status for discovered active targets"},
+			{"service-discovery", "labels before and after relabeling for discovered targets"},
 			{"api/v1/targets", "advanced information about discovered targets in JSON format"},
 			{"config", "-promscrape.config contents"},
 			{"metrics", "available service metrics"},
diff --git a/app/vmagent/README.md b/app/vmagent/README.md
index d6ca120b0..b11deb3a9 100644
--- a/app/vmagent/README.md
+++ b/app/vmagent/README.md
@@ -371,8 +371,12 @@ start a cluster of three `vmagent` instances, where each target is scraped by tw
 ```
 
 If each target is scraped by multiple `vmagent` instances, then data deduplication must be enabled at remote storage pointed by `-remoteWrite.url`.
+The `-dedup.minScrapeInterval` must be set to the `scrape_interval` configured at `-promscrape.config`.
 See [these docs](https://docs.victoriametrics.com/#deduplication) for details.
 
+If multiple `vmagent` clusters scrape the same set of targets, then each cluster must have unique value for the `-promscrape.cluster.name` command-line flag.
+This is needed for proper data de-duplication. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2679) for details.
+
 ## Scraping targets via a proxy
 
 `vmagent` supports scraping targets via http, https and socks5 proxies. Proxy address must be specified in `proxy_url` option. For example, the following scrape config instructs
diff --git a/app/vmagent/main.go b/app/vmagent/main.go
index 311296cca..c7b0e3b54 100644
--- a/app/vmagent/main.go
+++ b/app/vmagent/main.go
@@ -167,7 +167,8 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
 		fmt.Fprintf(w, "See docs at <a href='https://docs.victoriametrics.com/vmagent.html'>https://docs.victoriametrics.com/vmagent.html</a></br>")
 		fmt.Fprintf(w, "Useful endpoints:</br>")
 		httpserver.WriteAPIHelp(w, [][2]string{
-			{"targets", "discovered targets list"},
+			{"targets", "status for discovered active targets"},
+			{"service-discovery", "labels before and after relabeling for discovered targets"},
 			{"api/v1/targets", "advanced information about discovered targets in JSON format"},
 			{"config", "-promscrape.config contents"},
 			{"metrics", "available service metrics"},
@@ -271,6 +272,10 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
 		promscrapeTargetsRequests.Inc()
 		promscrape.WriteHumanReadableTargetsStatus(w, r)
 		return true
+	case "/service-discovery":
+		promscrapeServiceDiscoveryRequests.Inc()
+		promscrape.WriteServiceDiscovery(w, r)
+		return true
 	case "/target_response":
 		promscrapeTargetResponseRequests.Inc()
 		if err := promscrape.WriteTargetResponse(w, r); err != nil {
@@ -478,8 +483,9 @@ var (
 	datadogCheckRunRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/datadog/api/v1/check_run", protocol="datadog"}`)
 	datadogIntakeRequests   = metrics.NewCounter(`vmagent_http_requests_total{path="/datadog/intake/", protocol="datadog"}`)
 
-	promscrapeTargetsRequests      = metrics.NewCounter(`vmagent_http_requests_total{path="/targets"}`)
-	promscrapeAPIV1TargetsRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/api/v1/targets"}`)
+	promscrapeTargetsRequests          = metrics.NewCounter(`vmagent_http_requests_total{path="/targets"}`)
+	promscrapeServiceDiscoveryRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/service-discovery"}`)
+	promscrapeAPIV1TargetsRequests     = metrics.NewCounter(`vmagent_http_requests_total{path="/api/v1/targets"}`)
 
 	promscrapeTargetResponseRequests = metrics.NewCounter(`vmagent_http_requests_total{path="/target_response"}`)
 	promscrapeTargetResponseErrors   = metrics.NewCounter(`vmagent_http_request_errors_total{path="/target_response"}`)
diff --git a/app/vmalert/tpl/header.qtpl b/app/vmalert/tpl/header.qtpl
index 471b5499e..486521268 100644
--- a/app/vmalert/tpl/header.qtpl
+++ b/app/vmalert/tpl/header.qtpl
@@ -3,7 +3,7 @@
 <html lang="en">
 <head>
     <title>vmalert{% if title != "" %} - {%s title %}{% endif %}</title>
-    <link href="static/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
+    <link href="static/css/bootstrap.min.css" rel="stylesheet" />
     <style>
         body{
           min-height: 75rem;
diff --git a/app/vmalert/tpl/header.qtpl.go b/app/vmalert/tpl/header.qtpl.go
index b9011e3a8..09f05906f 100644
--- a/app/vmalert/tpl/header.qtpl.go
+++ b/app/vmalert/tpl/header.qtpl.go
@@ -35,7 +35,7 @@ func StreamHeader(qw422016 *qt422016.Writer, title string, pages []NavItem) {
 	}
 //line app/vmalert/tpl/header.qtpl:5
 	qw422016.N().S(`</title>
-    <link href="static/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
+    <link href="static/css/bootstrap.min.css" rel="stylesheet" />
     <style>
         body{
           min-height: 75rem;
diff --git a/app/vminsert/main.go b/app/vminsert/main.go
index 8159be659..7e641b088 100644
--- a/app/vminsert/main.go
+++ b/app/vminsert/main.go
@@ -1,6 +1,7 @@
 package vminsert
 
 import (
+	"embed"
 	"flag"
 	"fmt"
 	"net/http"
@@ -55,6 +56,11 @@ var (
 	opentsdbhttpServer *opentsdbhttpserver.Server
 )
 
+//go:embed static
+var staticFiles embed.FS
+
+var staticServer = http.FileServer(http.FS(staticFiles))
+
 // Init initializes vminsert.
 func Init() {
 	relabel.Init()
@@ -101,6 +107,15 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 	defer requestDuration.UpdateDuration(startTime)
 
 	path := strings.Replace(r.URL.Path, "//", "/", -1)
+	if strings.HasPrefix(path, "/static") {
+		staticServer.ServeHTTP(w, r)
+		return true
+	}
+	if strings.HasPrefix(path, "/prometheus/static") {
+		r.URL.Path = strings.TrimPrefix(path, "/prometheus")
+		staticServer.ServeHTTP(w, r)
+		return true
+	}
 	switch path {
 	case "/prometheus/api/v1/write", "/api/v1/write":
 		prometheusWriteRequests.Inc()
@@ -196,6 +211,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 		promscrapeTargetsRequests.Inc()
 		promscrape.WriteHumanReadableTargetsStatus(w, r)
 		return true
+	case "/prometheus/service-discovery", "/service-discovery":
+		promscrapeServiceDiscoveryRequests.Inc()
+		promscrape.WriteServiceDiscovery(w, r)
+		return true
 	case "/prometheus/api/v1/targets", "/api/v1/targets":
 		promscrapeAPIV1TargetsRequests.Inc()
 		w.Header().Set("Content-Type", "application/json")
@@ -296,8 +315,9 @@ var (
 	datadogCheckRunRequests = metrics.NewCounter(`vm_http_requests_total{path="/datadog/api/v1/check_run", protocol="datadog"}`)
 	datadogIntakeRequests   = metrics.NewCounter(`vm_http_requests_total{path="/datadog/intake/", protocol="datadog"}`)
 
-	promscrapeTargetsRequests      = metrics.NewCounter(`vm_http_requests_total{path="/targets"}`)
-	promscrapeAPIV1TargetsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/targets"}`)
+	promscrapeTargetsRequests          = metrics.NewCounter(`vm_http_requests_total{path="/targets"}`)
+	promscrapeServiceDiscoveryRequests = metrics.NewCounter(`vm_http_requests_total{path="/service-discovery"}`)
+	promscrapeAPIV1TargetsRequests     = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/targets"}`)
 
 	promscrapeTargetResponseRequests = metrics.NewCounter(`vm_http_requests_total{path="/target_response"}`)
 	promscrapeTargetResponseErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/target_response"}`)
diff --git a/app/vmselect/static/css/bootstrap.min.css b/app/vminsert/static/css/bootstrap.min.css
similarity index 100%
rename from app/vmselect/static/css/bootstrap.min.css
rename to app/vminsert/static/css/bootstrap.min.css
diff --git a/app/vmselect/main.go b/app/vmselect/main.go
index ff5d718a2..06d4b5e9e 100644
--- a/app/vmselect/main.go
+++ b/app/vmselect/main.go
@@ -86,13 +86,7 @@ var (
 //go:embed vmui
 var vmuiFiles embed.FS
 
-//go:embed static
-var staticFiles embed.FS
-
-var (
-	vmuiFileServer = http.FileServer(http.FS(vmuiFiles))
-	staticServer   = http.FileServer(http.FS(staticFiles))
-)
+var vmuiFileServer = http.FileServer(http.FS(vmuiFiles))
 
 // RequestHandler handles remote read API requests
 func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
@@ -187,11 +181,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 		vmuiFileServer.ServeHTTP(w, r)
 		return true
 	}
-	if strings.HasPrefix(path, "/static") {
-		staticServer.ServeHTTP(w, r)
-		return true
-	}
-
 	if strings.HasPrefix(path, "/api/v1/label/") {
 		s := path[len("/api/v1/label/"):]
 		if strings.HasSuffix(s, "/values") {
diff --git a/docs/Articles.md b/docs/Articles.md
index 315a03fbf..5b43b844a 100644
--- a/docs/Articles.md
+++ b/docs/Articles.md
@@ -8,6 +8,7 @@ See also [case studies](https://docs.victoriametrics.com/CaseStudies.html).
 
 ## Third-party articles and slides about VictoriaMetrics
 
+* [Optimizing the Storage of Large Volumes of Metrics for a Long Time in VictoriaMetrics](https://percona.community/blog/2022/06/02/long-time-keeping-metrics-victoriametrics/)
 * [Announcing Asserts](https://www.asserts.ai/blog/announcing-asserts/)
 * [Choosing a Time Series Database for High Cardinality Aggregations](https://abiosgaming.com/press/high-cardinality-aggregations/)
 * [Scaling to trillions of metric data points](https://engineering.razorpay.com/scaling-to-trillions-of-metric-data-points-f569a5b654f2)
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index a66a2b289..f672f0ba3 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -15,15 +15,17 @@ The following tip changes can be tested by building VictoriaMetrics components f
 
 ## tip
 
-* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): Allows using kubeconfig file within `kubernetes_sd_configs`. It may be useful for kubernetes cluster monitoring by `vmagent` outside kubernetes cluster. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1464).
 * FEATURE: allow overriding default limits for in-memory cache `indexdb/tagFilters` via flag `-storage.cacheSizeIndexDBTagFilters`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2663).
 * FEATURE: add support of `lowercase` and `uppercase` relabeling actions in the same way as [Prometheus 2.36.0 does](https://github.com/prometheus/prometheus/releases/tag/v2.36.0). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2664).
 * FEATURE: support query tracing, which allows determining bottlenecks during query processing. See [these docs](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#query-tracing) and [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1403).
-* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): remove dependency on Internet access in `http://vmagent:8429/targets` page. Previously the page layout was broken without Internet access. See [shis issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2594).
-* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): remove dependency on Internet access in [web API pages](https://docs.victoriametrics.com/vmalert.html#web). Previously the functionality and the layout of these pages was broken without Internet access. See [shis issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2594).
-* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): expose `/api/v1/status/config` endpoint in the same way as Prometheus does. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/api/#config).
 * FEATURE: add ability to change the `indexdb` rotation timezone offset via `-retentionTimezoneOffset` command-line flag. Previously it was performed at 4am UTC time. This could lead to performance degradation in the middle of the day when VictoriaMetrics runs in time zones located too far from UTC. Thanks to @cnych for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2574).
+* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): remove dependency on Internet access at [web API pages](https://docs.victoriametrics.com/vmalert.html#web). Previously the functionality and the layout of these pages was broken without Internet access. See [shis issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2594).
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): implement the `http://vmagent:8429/service-discovery` page in the same way as Prometheus does. This page shows the original labels for all the discovered targets alongside the resulting labels after the relabeling. This simplifies service discovery debugging.
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): remove dependency on Internet access at `http://vmagent:8429/targets` page. Previously the page layout was broken without Internet access. See [shis issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2594).
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for `kubeconfig_file` option at [kubernetes_sd_configs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kubernetes_sd_config). It may be useful for Kubernetes monitoring by `vmagent` outside Kubernetes cluster. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1464).
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): expose `/api/v1/status/config` endpoint in the same way as Prometheus does. See [these docs](https://prometheus.io/docs/prometheus/latest/querying/api/#config).
 * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add `-promscrape.suppressScrapeErrorsDelay` command-line flag, which can be used for delaying and aggregating the logging of per-target scrape errors. This may reduce the amounts of logs when `vmagent` scrapes many unreliable targets. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2575). Thanks to @jelmd for [the initial implementation](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2576).
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add `-promscrape.cluster.name` command-line flag, which allows proper data de-duplication when the same target is scraped from multiple [vmagent clusters](https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2679).
 
 * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): properly apply `alert_relabel_configs` relabeling rules to `-notifier.config` according to [these docs](https://docs.victoriametrics.com/vmalert.html#notifier-configuration-file). Thanks to @spectvtor for [the bugfix](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2633).
 * BUGFIX: deny [background merge](https://valyala.medium.com/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282) when the storage enters read-only mode, e.g. when free disk space becomes lower than `-storage.minFreeDiskSpaceBytes`. Background merge needs additional disk space, so it could result in `no space left on device` errors. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2603).
diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md
index f300f1ea7..242189f89 100644
--- a/docs/Cluster-VictoriaMetrics.md
+++ b/docs/Cluster-VictoriaMetrics.md
@@ -8,18 +8,18 @@ sort: 2
 
 VictoriaMetrics is a fast, cost-effective and scalable time series database. It can be used as a long-term remote storage for Prometheus.
 
-It is recommended using [single-node version](https://github.com/VictoriaMetrics/VictoriaMetrics) instead of cluster version
-for ingestion rates lower than a million of data points per second.
-Single-node version [scales perfectly](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae)
+It is recommended to use the [single-node version](https://github.com/VictoriaMetrics/VictoriaMetrics) instead of the cluster version
+for ingestion rates lower than a million data points per second.
+The single-node version [scales perfectly](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae)
 with the number of CPU cores, RAM and available storage space.
-Single-node version is easier to configure and operate comparing to cluster version, so think twice before sticking to cluster version.
+The single-node version is easier to configure and operate compared to the cluster version, so think twice before choosing the cluster version.
 
 Join [our Slack](https://slack.victoriametrics.com/) or [contact us](mailto:info@victoriametrics.com) with consulting and support questions.
 
 ## Prominent features
 
-- Supports all the features of [single-node version](https://github.com/VictoriaMetrics/VictoriaMetrics).
-- Performance and capacity scales horizontally. See [these docs for details](#cluster-resizing-and-scalability).
+- Supports all the features of the [single-node version](https://github.com/VictoriaMetrics/VictoriaMetrics).
+- Performance and capacity scale horizontally. See [these docs for details](#cluster-resizing-and-scalability).
 - Supports multiple independent namespaces for time series data (aka multi-tenancy). See [these docs for details](#multitenancy).
 - Supports replication. See [these docs for details](#replication-and-data-safety).
 
@@ -33,8 +33,8 @@ VictoriaMetrics cluster consists of the following services:
 
 Each service may scale independently and may run on the most suitable hardware.
 `vmstorage` nodes don't know about each other, don't communicate with each other and don't share any data.
-This is [shared nothing architecture](https://en.wikipedia.org/wiki/Shared-nothing_architecture).
-It increases cluster availability, simplifies cluster maintenance and cluster scaling.
+This is a [shared nothing architecture](https://en.wikipedia.org/wiki/Shared-nothing_architecture).
+It increases cluster availability, and simplifies cluster maintenance as well as cluster scaling.
 
 ![Naive cluster scheme](assets/images/Naive_cluster_scheme.png)
 
@@ -60,10 +60,10 @@ when different tenants have different amounts of data and different query load.
 
 ## Binaries
 
-Compiled binaries for cluster version are available in the `assets` section of [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
-See archives containing `cluster` word.
+Compiled binaries for the cluster version are available in the `assets` section of the [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
+Also see archives containing the word `cluster`.
 
-Docker images for cluster version are available here:
+Docker images for the cluster version are available here:
 
 - `vminsert` - <https://hub.docker.com/r/victoriametrics/vminsert/tags>
 - `vmselect` - <https://hub.docker.com/r/victoriametrics/vmselect/tags>
@@ -71,20 +71,20 @@ Docker images for cluster version are available here:
 
 ## Building from sources
 
-Source code for cluster version is available at [cluster branch](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
+The source code for the cluster version is available in the [cluster branch](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
 
 ### Production builds
 
-There is no need in installing Go on a host system since binaries are built
+There is no need to install Go on a host system since binaries are built
 inside [the official docker container for Go](https://hub.docker.com/_/golang).
-This makes reproducible builds.
+This allows reproducible builds.
 So [install docker](https://docs.docker.com/install/) and run the following command:
 
 ```
 make vminsert-prod vmselect-prod vmstorage-prod
 ```
 
-Production binaries are built into statically linked binaries. They are put into `bin` folder with `-prod` suffixes:
+Production binaries are built into statically linked binaries. They are put into the `bin` folder with `-prod` suffixes:
 
 ```
 $ make vminsert-prod vmselect-prod vmstorage-prod
@@ -154,7 +154,7 @@ It is possible manualy setting up a toy cluster on a single host. In this case e
 
 ### Environment variables
 
-Each flag values can be set thru environment variables by following these rules:
+Each flag values can be set through environment variables by following these rules:
 
 - The `-envflag.enable` flag must be set
 - Each `.` in flag names must be substituted by `_` (for example `-insert.maxQueueDuration <duration>` will translate to `insert_maxQueueDuration=<duration>`)
diff --git a/docs/README.md b/docs/README.md
index 802405aba..0800119aa 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -17,10 +17,10 @@ VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMet
 and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). 
 Just download VictoriaMetrics and follow [these instructions](https://docs.victoriametrics.com/Quick-Start.html).
 
-Cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+The cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
 
-Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow 
-[QuickStart guide](https://docs.victoriametrics.com/Quick-Start.html) for better experience.
+Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow the 
+[QuickStart guide](https://docs.victoriametrics.com/Quick-Start.html) for a better experience.
 
 [Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. 
 See [features available in enterprise package](https://victoriametrics.com/products/enterprise/).
@@ -32,8 +32,8 @@ from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/rele
 VictoriaMetrics has the following prominent features:
 
 * It can be used as long-term storage for Prometheus. See [these docs](#prometheus-setup) for details.
-* It can be used as drop-in replacement for Prometheus in Grafana, because it supports [Prometheus querying API](#prometheus-querying-api-usage).
-* It can be used as drop-in replacement for Graphite in Grafana, because it supports [Graphite API](#graphite-api-usage).
+* It can be used as a drop-in replacement for Prometheus in Grafana, because it supports [Prometheus querying API](#prometheus-querying-api-usage).
+* It can be used as  a drop-in replacement for Graphite in Grafana, because it supports [Graphite API](#graphite-api-usage).
 * It features easy setup and operation:
   * VictoriaMetrics consists of a single [small executable](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d) without external dependencies.
   * All the configuration is done via explicit command-line flags with reasonable defaults.
@@ -824,6 +824,11 @@ Each JSON line contains samples for a single time series. An example output:
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 Optional `max_rows_per_line` arg may be added to the request for limiting the maximum number of rows exported per each JSON line.
 Optional `reduce_mem_usage=1` arg may be added to the request for reducing memory usage when exporting big number of time series.
@@ -863,6 +868,11 @@ for metrics to export.
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export/csv -d 'format=<format>' -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export/csv -d 'format=<format>' -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 The exported CSV data can be imported to VictoriaMetrics via [/api/v1/import/csv](#how-to-import-csv-data).
 
@@ -885,6 +895,11 @@ wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export/native -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export/native -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 The exported data can be imported to VictoriaMetrics via [/api/v1/import/native](#how-to-import-data-in-native-format).
 The native export format may change in incompatible way between VictoriaMetrics releases, so the data exported from the release X
@@ -1079,8 +1094,13 @@ VictoriaMetrics exports [Prometheus-compatible federation data](https://promethe
 at `http://<victoriametrics-addr>:8428/federate?match[]=<timeseries_selector_for_federation>`.
 
 Optional `start` and `end` args may be added to the request in order to scrape the last point for each selected time series on the `[start ... end]` interval.
-`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values. By default, the last point
-on the interval `[now - max_lookback ... now]` is scraped for each time series. The default value for `max_lookback` is `5m` (5 minutes), but it can be overridden.
+`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/federate -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/federate -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
+By default, the last point on the interval `[now - max_lookback ... now]` is scraped for each time series. The default value for `max_lookback` is `5m` (5 minutes), but it can be overridden with `max_lookback` query arg.
 For instance, `/federate?match[]=up&max_lookback=1h` would return last points on the `[now - 1h ... now]` interval. This may be useful for time series federation
 with scrape intervals exceeding `5m`.
 
@@ -1259,7 +1279,7 @@ The downsampling can be evaluated for free by downloading and using enterprise b
 
 ## Multi-tenancy
 
-Single-node VictoriaMetrics doesn't support multi-tenancy. Use [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy) instead.
+Single-node VictoriaMetrics doesn't support multi-tenancy. Use the [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy) instead.
 
 ## Scalability and cluster version
 
@@ -1267,7 +1287,7 @@ Though single-node VictoriaMetrics cannot scale to multiple nodes, it is optimiz
 This means that a single-node VictoriaMetrics may scale vertically and substitute a moderately sized cluster built with competing solutions
 such as Thanos, Uber M3, InfluxDB or TimescaleDB. See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
 
-So try single-node VictoriaMetrics at first and then [switch to cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
+So try single-node VictoriaMetrics at first and then [switch to the cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
 horizontally scalable long-term remote storage for really large Prometheus deployments.
 [Contact us](mailto:info@victoriametrics.com) for enterprise support.
 
@@ -1342,7 +1362,7 @@ The most interesting metrics are:
   aka [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
 * `increase(vm_new_timeseries_created_total[1h])` - time series [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) during the previous hour.
 * `sum(vm_rows{type=~"storage/.*"})` - total number of `(timestamp, value)` data points in the database.
-* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted int the database per second.
+* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted in the database per second.
 * `vm_free_disk_space_bytes` - free space left at `-storageDataPath`.
 * `sum(vm_data_size_bytes)` - the total size of data on disk.
 * `increase(vm_slow_row_inserts_total[5m])` - the number of slow inserts during the last 5 minutes.
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index ca4002c27..8f0363e17 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -21,10 +21,10 @@ VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMet
 and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). 
 Just download VictoriaMetrics and follow [these instructions](https://docs.victoriametrics.com/Quick-Start.html).
 
-Cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+The cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
 
-Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow 
-[QuickStart guide](https://docs.victoriametrics.com/Quick-Start.html) for better experience.
+Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow the 
+[QuickStart guide](https://docs.victoriametrics.com/Quick-Start.html) for a better experience.
 
 [Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. 
 See [features available in enterprise package](https://victoriametrics.com/products/enterprise/).
@@ -36,8 +36,8 @@ from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/rele
 VictoriaMetrics has the following prominent features:
 
 * It can be used as long-term storage for Prometheus. See [these docs](#prometheus-setup) for details.
-* It can be used as drop-in replacement for Prometheus in Grafana, because it supports [Prometheus querying API](#prometheus-querying-api-usage).
-* It can be used as drop-in replacement for Graphite in Grafana, because it supports [Graphite API](#graphite-api-usage).
+* It can be used as a drop-in replacement for Prometheus in Grafana, because it supports [Prometheus querying API](#prometheus-querying-api-usage).
+* It can be used as  a drop-in replacement for Graphite in Grafana, because it supports [Graphite API](#graphite-api-usage).
 * It features easy setup and operation:
   * VictoriaMetrics consists of a single [small executable](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d) without external dependencies.
   * All the configuration is done via explicit command-line flags with reasonable defaults.
@@ -828,6 +828,11 @@ Each JSON line contains samples for a single time series. An example output:
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 Optional `max_rows_per_line` arg may be added to the request for limiting the maximum number of rows exported per each JSON line.
 Optional `reduce_mem_usage=1` arg may be added to the request for reducing memory usage when exporting big number of time series.
@@ -867,6 +872,11 @@ for metrics to export.
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export/csv -d 'format=<format>' -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export/csv -d 'format=<format>' -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 The exported CSV data can be imported to VictoriaMetrics via [/api/v1/import/csv](#how-to-import-csv-data).
 
@@ -889,6 +899,11 @@ wget -O- -q 'http://your_victoriametrics_instance:8428/api/v1/series/count' | jq
 
 Optional `start` and `end` args may be added to the request in order to limit the time frame for the exported data. These args may contain either
 unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/api/v1/export/native -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/api/v1/export/native -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
 
 The exported data can be imported to VictoriaMetrics via [/api/v1/import/native](#how-to-import-data-in-native-format).
 The native export format may change in incompatible way between VictoriaMetrics releases, so the data exported from the release X
@@ -1083,8 +1098,13 @@ VictoriaMetrics exports [Prometheus-compatible federation data](https://promethe
 at `http://<victoriametrics-addr>:8428/federate?match[]=<timeseries_selector_for_federation>`.
 
 Optional `start` and `end` args may be added to the request in order to scrape the last point for each selected time series on the `[start ... end]` interval.
-`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values. By default, the last point
-on the interval `[now - max_lookback ... now]` is scraped for each time series. The default value for `max_lookback` is `5m` (5 minutes), but it can be overridden.
+`start` and `end` may contain either unix timestamp in seconds or [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) values.
+For example:
+```bash
+curl http://<victoriametrics-addr>:8428/federate -d 'match[]=<timeseries_selector_for_export>' -d 'start=1654543486' -d 'end=1654543486'
+curl http://<victoriametrics-addr>:8428/federate -d 'match[]=<timeseries_selector_for_export>' -d 'start=2022-06-06T19:25:48+00:00' -d 'end=2022-06-06T19:29:07+00:00'
+```
+By default, the last point on the interval `[now - max_lookback ... now]` is scraped for each time series. The default value for `max_lookback` is `5m` (5 minutes), but it can be overridden with `max_lookback` query arg.
 For instance, `/federate?match[]=up&max_lookback=1h` would return last points on the `[now - 1h ... now]` interval. This may be useful for time series federation
 with scrape intervals exceeding `5m`.
 
@@ -1263,7 +1283,7 @@ The downsampling can be evaluated for free by downloading and using enterprise b
 
 ## Multi-tenancy
 
-Single-node VictoriaMetrics doesn't support multi-tenancy. Use [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy) instead.
+Single-node VictoriaMetrics doesn't support multi-tenancy. Use the [cluster version](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#multitenancy) instead.
 
 ## Scalability and cluster version
 
@@ -1271,7 +1291,7 @@ Though single-node VictoriaMetrics cannot scale to multiple nodes, it is optimiz
 This means that a single-node VictoriaMetrics may scale vertically and substitute a moderately sized cluster built with competing solutions
 such as Thanos, Uber M3, InfluxDB or TimescaleDB. See [vertical scalability benchmarks](https://medium.com/@valyala/measuring-vertical-scalability-for-time-series-databases-in-google-cloud-92550d78d8ae).
 
-So try single-node VictoriaMetrics at first and then [switch to cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
+So try single-node VictoriaMetrics at first and then [switch to the cluster version](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster) if you still need
 horizontally scalable long-term remote storage for really large Prometheus deployments.
 [Contact us](mailto:info@victoriametrics.com) for enterprise support.
 
@@ -1346,7 +1366,7 @@ The most interesting metrics are:
   aka [active time series](https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series).
 * `increase(vm_new_timeseries_created_total[1h])` - time series [churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) during the previous hour.
 * `sum(vm_rows{type=~"storage/.*"})` - total number of `(timestamp, value)` data points in the database.
-* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted int the database per second.
+* `sum(rate(vm_rows_inserted_total[5m]))` - ingestion rate, i.e. how many samples are inserted in the database per second.
 * `vm_free_disk_space_bytes` - free space left at `-storageDataPath`.
 * `sum(vm_data_size_bytes)` - the total size of data on disk.
 * `increase(vm_slow_row_inserts_total[5m])` - the number of slow inserts during the last 5 minutes.
diff --git a/docs/vmagent.md b/docs/vmagent.md
index 257ee8bed..53e134d53 100644
--- a/docs/vmagent.md
+++ b/docs/vmagent.md
@@ -375,8 +375,12 @@ start a cluster of three `vmagent` instances, where each target is scraped by tw
 ```
 
 If each target is scraped by multiple `vmagent` instances, then data deduplication must be enabled at remote storage pointed by `-remoteWrite.url`.
+The `-dedup.minScrapeInterval` must be set to the `scrape_interval` configured at `-promscrape.config`.
 See [these docs](https://docs.victoriametrics.com/#deduplication) for details.
 
+If multiple `vmagent` clusters scrape the same set of targets, then each cluster must have unique value for the `-promscrape.cluster.name` command-line flag.
+This is needed for proper data de-duplication. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2679) for details.
+
 ## Scraping targets via a proxy
 
 `vmagent` supports scraping targets via http, https and socks5 proxies. Proxy address must be specified in `proxy_url` option. For example, the following scrape config instructs
diff --git a/go.mod b/go.mod
index 7ae82f759..a3237f44c 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/VictoriaMetrics/fasthttp v1.1.0
 	github.com/VictoriaMetrics/metrics v1.18.1
 	github.com/VictoriaMetrics/metricsql v0.43.0
-	github.com/aws/aws-sdk-go v1.44.24
+	github.com/aws/aws-sdk-go v1.44.27
 	github.com/cespare/xxhash/v2 v2.1.2
 
 	// TODO: switch back to https://github.com/cheggaaa/pb/v3 when v3-pooling branch
@@ -20,7 +20,7 @@ require (
 	github.com/dmitryk-dk/pb/v3 v3.0.9
 	github.com/golang/snappy v0.0.4
 	github.com/influxdata/influxdb v1.9.7
-	github.com/klauspost/compress v1.15.5
+	github.com/klauspost/compress v1.15.6
 	github.com/prometheus/prometheus v1.8.2-0.20201119142752-3ad25a6dc3d9
 	github.com/urfave/cli/v2 v2.8.1
 	github.com/valyala/fastjson v1.6.3
@@ -28,10 +28,10 @@ require (
 	github.com/valyala/fasttemplate v1.2.1
 	github.com/valyala/gozstd v1.17.0
 	github.com/valyala/quicktemplate v1.7.0
-	golang.org/x/net v0.0.0-20220526153639-5463443f8c37
+	golang.org/x/net v0.0.0-20220531201128-c960675eff93
 	golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
-	google.golang.org/api v0.81.0
+	google.golang.org/api v0.82.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
@@ -71,12 +71,12 @@ require (
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 // indirect
-	golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
+	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
-	google.golang.org/grpc v1.46.2 // indirect
+	google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
+	google.golang.org/grpc v1.47.0 // indirect
 	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/go.sum b/go.sum
index 3cb2ec721..dc9a5a603 100644
--- a/go.sum
+++ b/go.sum
@@ -142,8 +142,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.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 github.com/aws/aws-sdk-go v1.35.31/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
-github.com/aws/aws-sdk-go v1.44.24 h1:3nOkwJBJLiGBmJKWp3z0utyXuBkxyGkRRwWjrTItJaY=
-github.com/aws/aws-sdk-go v1.44.24/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/aws/aws-sdk-go v1.44.27 h1:8CMspeZSrewnbvAwgl8qo5R7orDLwQnTGBf/OKPiHxI=
+github.com/aws/aws-sdk-go v1.44.27/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=
@@ -566,8 +566,8 @@ github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
 github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.15.5 h1:qyCLMz2JCrKADihKOh9FxnW3houKeNsp2h5OEz0QSEA=
-github.com/klauspost/compress v1.15.5/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
+github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
 github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -992,9 +992,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
 golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
+golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1028,8 +1028,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
 golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1267,8 +1268,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR
 google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
 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.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
-google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
+google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw=
+google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os=
 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=
@@ -1356,10 +1357,10 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX
 google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
 google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
+google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM=
+google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -1394,8 +1395,9 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
 google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 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 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
 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/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=
diff --git a/lib/promauth/config.go b/lib/promauth/config.go
index bb54ef7d6..5f48bf66f 100644
--- a/lib/promauth/config.go
+++ b/lib/promauth/config.go
@@ -15,6 +15,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+	xxhash "github.com/cespare/xxhash/v2"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/clientcredentials"
 )
@@ -84,8 +85,11 @@ func (tlsConfig *TLSConfig) String() string {
 	if tlsConfig == nil {
 		return ""
 	}
-	return fmt.Sprintf("ca_file=%q, cert_file=%q, key_file=%q, server_name=%q, insecure_skip_verify=%v, min_version=%q",
-		tlsConfig.CAFile, tlsConfig.CertFile, tlsConfig.KeyFile, tlsConfig.ServerName, tlsConfig.InsecureSkipVerify, tlsConfig.MinVersion)
+	caHash := xxhash.Sum64(tlsConfig.CA)
+	certHash := xxhash.Sum64(tlsConfig.Cert)
+	keyHash := xxhash.Sum64(tlsConfig.Key)
+	return fmt.Sprintf("hash(ca)=%d, ca_file=%q, hash(cert)=%d, cert_file=%q, hash(key)=%d, key_file=%q, server_name=%q, insecure_skip_verify=%v, min_version=%q",
+		caHash, tlsConfig.CAFile, certHash, tlsConfig.CertFile, keyHash, tlsConfig.KeyFile, tlsConfig.ServerName, tlsConfig.InsecureSkipVerify, tlsConfig.MinVersion)
 }
 
 // Authorization represents generic authorization config.
@@ -273,6 +277,9 @@ func (ac *Config) GetAuthHeader() string {
 }
 
 // String returns human-readable representation for ac.
+//
+// It is also used for comparing Config objects for equality. If two Config
+// objects have the same string representation, then they are considered equal.
 func (ac *Config) String() string {
 	return fmt.Sprintf("AuthDigest=%s, TLSRootCA=%s, TLSCertificate=%s, TLSServerName=%s, TLSInsecureSkipVerify=%v, TLSMinVersion=%d",
 		ac.authDigest, ac.tlsRootCAString(), ac.tlsCertDigest, ac.TLSServerName, ac.TLSInsecureSkipVerify, ac.TLSMinVersion)
@@ -459,21 +466,24 @@ func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, be
 	if tlsConfig != nil {
 		tlsServerName = tlsConfig.ServerName
 		tlsInsecureSkipVerify = tlsConfig.InsecureSkipVerify
-		if (tlsConfig.CertFile != "" || tlsConfig.KeyFile != "") || (len(tlsConfig.Key) != 0 || len(tlsConfig.Cert) != 0) {
+		if len(tlsConfig.Key) != 0 || len(tlsConfig.Cert) != 0 {
+			cert, err := tls.X509KeyPair(tlsConfig.Cert, tlsConfig.Key)
+			if err != nil {
+				return nil, fmt.Errorf("cannot load TLS certificate from the provided `cert` and `key` values: %w", err)
+			}
+			getTLSCert = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
+				return &cert, nil
+			}
+			h := xxhash.Sum64(tlsConfig.Key) ^ xxhash.Sum64(tlsConfig.Cert)
+			tlsCertDigest = fmt.Sprintf("digest(key+cert)=%d", h)
+		} else if tlsConfig.CertFile != "" || tlsConfig.KeyFile != "" {
 			getTLSCert = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
 				// Re-read TLS certificate from disk. This is needed for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1420
-				if tlsConfig.CertFile != "" || tlsConfig.KeyFile != "" {
-					certPath := fs.GetFilepath(baseDir, tlsConfig.CertFile)
-					keyPath := fs.GetFilepath(baseDir, tlsConfig.KeyFile)
-					cert, err := tls.LoadX509KeyPair(certPath, keyPath)
-					if err != nil {
-						return nil, fmt.Errorf("cannot load TLS certificate from `cert_file`=%q, `key_file`=%q: %w", tlsConfig.CertFile, tlsConfig.KeyFile, err)
-					}
-					return &cert, nil
-				}
-				cert, err := tls.X509KeyPair(tlsConfig.Cert, tlsConfig.Key)
+				certPath := fs.GetFilepath(baseDir, tlsConfig.CertFile)
+				keyPath := fs.GetFilepath(baseDir, tlsConfig.KeyFile)
+				cert, err := tls.LoadX509KeyPair(certPath, keyPath)
 				if err != nil {
-					return nil, fmt.Errorf("cannot load TLS certificate: %w", err)
+					return nil, fmt.Errorf("cannot load TLS certificate from `cert_file`=%q, `key_file`=%q: %w", tlsConfig.CertFile, tlsConfig.KeyFile, err)
 				}
 				return &cert, nil
 			}
@@ -483,17 +493,19 @@ func NewConfig(baseDir string, az *Authorization, basicAuth *BasicAuthConfig, be
 			}
 			tlsCertDigest = fmt.Sprintf("certFile=%q, keyFile=%q", tlsConfig.CertFile, tlsConfig.KeyFile)
 		}
-		if tlsConfig.CAFile != "" {
+		if len(tlsConfig.CA) != 0 {
+			tlsRootCA = x509.NewCertPool()
+			if !tlsRootCA.AppendCertsFromPEM(tlsConfig.CA) {
+				return nil, fmt.Errorf("cannot parse data from `ca` value")
+			}
+		} else if tlsConfig.CAFile != "" {
 			path := fs.GetFilepath(baseDir, tlsConfig.CAFile)
 			data, err := fs.ReadFileOrHTTP(path)
 			if err != nil {
 				return nil, fmt.Errorf("cannot read `ca_file` %q: %w", tlsConfig.CAFile, err)
 			}
-			tlsConfig.CA = data
-		}
-		if len(tlsConfig.CA) != 0 {
 			tlsRootCA = x509.NewCertPool()
-			if !tlsRootCA.AppendCertsFromPEM(tlsConfig.CA) {
+			if !tlsRootCA.AppendCertsFromPEM(data) {
 				return nil, fmt.Errorf("cannot parse data from `ca_file` %q", tlsConfig.CAFile)
 			}
 		}
diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go
index f73c6b706..acba6d7b5 100644
--- a/lib/promscrape/config.go
+++ b/lib/promscrape/config.go
@@ -56,6 +56,9 @@ var (
 		"Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name")
 	clusterReplicationFactor = flag.Int("promscrape.cluster.replicationFactor", 1, "The number of members in the cluster, which scrape the same targets. "+
 		"If the replication factor is greater than 1, then the deduplication must be enabled at remote storage side. See https://docs.victoriametrics.com/#deduplication")
+	clusterName = flag.String("promscrape.cluster.name", "", "Optional name of the cluster. If multiple vmagent clusters scrape the same targets, "+
+		"then each cluster must have unique name in order to properly de-duplicate samples received from these clusters. "+
+		"See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2679")
 )
 
 var clusterMemberID int
diff --git a/lib/promscrape/discovery/kubernetes/api.go b/lib/promscrape/discovery/kubernetes/api.go
index 36ffa67be..8950e623d 100644
--- a/lib/promscrape/discovery/kubernetes/api.go
+++ b/lib/promscrape/discovery/kubernetes/api.go
@@ -22,16 +22,19 @@ func newAPIConfig(sdc *SDConfig, baseDir string, swcFunc ScrapeWorkConstructorFu
 	}
 	apiServer := sdc.APIServer
 
-	if len(sdc.KubeConfig) > 0 {
-		fmt.Println("building")
-		kc, err := buildConfig(sdc)
+	if len(sdc.KubeConfigFile) > 0 {
+		if len(apiServer) > 0 {
+			return nil, fmt.Errorf("`api_server: %q` and `kubeconfig_file: %q` options cannot be set simultaneously", apiServer, sdc.KubeConfigFile)
+		}
+		kc, err := newKubeConfig(sdc.KubeConfigFile)
 		if err != nil {
-			return nil, fmt.Errorf("cannot build kube config: %w", err)
+			return nil, fmt.Errorf("cannot build kube config from the specified `kubeconfig_file` config option: %w", err)
 		}
-		ac, err = promauth.NewConfig(".", nil, kc.basicAuth, kc.token, kc.tokenFile, nil, kc.tlsConfig)
+		acNew, err := promauth.NewConfig(".", nil, kc.basicAuth, kc.token, kc.tokenFile, nil, kc.tlsConfig)
 		if err != nil {
-			return nil, fmt.Errorf("cannot initialize service account auth: %w; probably, `kubernetes_sd_config->api_server` is missing in Prometheus configs?", err)
+			return nil, fmt.Errorf("cannot initialize auth config from `kubeconfig_file: %q`: %w", sdc.KubeConfigFile, err)
 		}
+		ac = acNew
 		apiServer = kc.server
 		sdc.ProxyURL = kc.proxyURL
 	}
diff --git a/lib/promscrape/discovery/kubernetes/kubeconfig.go b/lib/promscrape/discovery/kubernetes/kubeconfig.go
index 0971640fa..a59c43371 100644
--- a/lib/promscrape/discovery/kubernetes/kubeconfig.go
+++ b/lib/promscrape/discovery/kubernetes/kubeconfig.go
@@ -66,21 +66,20 @@ type AuthInfo struct {
 }
 
 func (au *AuthInfo) validate() error {
-	errContext := "field: %s is not supported currently, open an issue with feature request for it"
 	if au.Exec != nil {
-		return fmt.Errorf(errContext, "exec")
+		return unsupportedFieldError("exec")
 	}
 	if len(au.ImpersonateUID) > 0 {
-		return fmt.Errorf(errContext, "act-as-uid")
+		return unsupportedFieldError("act-as-uid")
 	}
 	if len(au.Impersonate) > 0 {
-		return fmt.Errorf(errContext, "act-as")
+		return unsupportedFieldError("act-as")
 	}
 	if len(au.ImpersonateGroups) > 0 {
-		return fmt.Errorf(errContext, "act-as-groups")
+		return unsupportedFieldError("act-as-groups")
 	}
 	if len(au.ImpersonateUserExtra) > 0 {
-		return fmt.Errorf(errContext, "act-as-user-extra")
+		return unsupportedFieldError("act-as-user-extra")
 	}
 	if len(au.Password) > 0 && len(au.Username) == 0 {
 		return fmt.Errorf("username cannot be empty, if password defined")
@@ -88,6 +87,11 @@ func (au *AuthInfo) validate() error {
 	return nil
 }
 
+func unsupportedFieldError(fieldName string) error {
+	return fmt.Errorf("field %q is not supported yet; if you feel it is needed please open a feature request "+
+		"at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new", fieldName)
+}
+
 // ExecConfig contains information about os.command, that returns auth token for kubernetes cluster connection
 type ExecConfig struct {
 	// Command to execute.
@@ -150,94 +154,95 @@ type kubeConfig struct {
 	proxyURL  *proxy.URL
 }
 
-func buildConfig(sdc *SDConfig) (*kubeConfig, error) {
-
-	data, err := fs.ReadFileOrHTTP(sdc.KubeConfig)
+func newKubeConfig(kubeConfigFile string) (*kubeConfig, error) {
+	data, err := fs.ReadFileOrHTTP(kubeConfigFile)
 	if err != nil {
-		return nil, fmt.Errorf("cannot read kubeConfig from %q: %w", sdc.KubeConfig, err)
+		return nil, fmt.Errorf("cannot read %q: %w", kubeConfigFile, err)
 	}
-	var config Config
-	if err = yaml.Unmarshal(data, &config); err != nil {
-		return nil, fmt.Errorf("cannot parse %q: %w", sdc.KubeConfig, err)
+	var cfg Config
+	if err = yaml.Unmarshal(data, &cfg); err != nil {
+		return nil, fmt.Errorf("cannot parse %q: %w", kubeConfigFile, err)
 	}
+	kc, err := cfg.buildKubeConfig()
+	if err != nil {
+		return nil, fmt.Errorf("cannot build kubeConfig from %q: %w", kubeConfigFile, err)
+	}
+	return kc, nil
+}
 
+func (cfg *Config) buildKubeConfig() (*kubeConfig, error) {
 	authInfos := make(map[string]*AuthInfo)
-	for _, obj := range config.AuthInfos {
+	for _, obj := range cfg.AuthInfos {
 		authInfos[obj.Name] = obj.AuthInfo
 	}
 	clusterInfos := make(map[string]*Cluster)
-	for _, obj := range config.Clusters {
+	for _, obj := range cfg.Clusters {
 		clusterInfos[obj.Name] = obj.Cluster
 	}
 	contexts := make(map[string]*Context)
-	for _, obj := range config.Contexts {
+	for _, obj := range cfg.Contexts {
 		contexts[obj.Name] = obj.Context
 	}
 
-	contextName := config.CurrentContext
+	contextName := cfg.CurrentContext
 	configContext := contexts[contextName]
 	if configContext == nil {
-		return nil, fmt.Errorf("context %q does not exist", contextName)
+		return nil, fmt.Errorf("missing context %q", contextName)
 	}
 
 	clusterInfoName := configContext.Cluster
 	configClusterInfo := clusterInfos[clusterInfoName]
 	if configClusterInfo == nil {
-		return nil, fmt.Errorf("cluster %q does not exist", clusterInfoName)
+		return nil, fmt.Errorf("missing cluster config %q at context %q", clusterInfoName, contextName)
 	}
-
-	if len(configClusterInfo.Server) == 0 {
-		return nil, fmt.Errorf("kubernetes server address cannot be empty, define it for context: %s", contextName)
+	server := configClusterInfo.Server
+	if len(server) == 0 {
+		return nil, fmt.Errorf("missing kubernetes server address for config %q at context %q", clusterInfoName, contextName)
 	}
 
 	authInfoName := configContext.AuthInfo
 	configAuthInfo := authInfos[authInfoName]
 	if authInfoName != "" && configAuthInfo == nil {
-		return nil, fmt.Errorf("auth info %q does not exist", authInfoName)
+		return nil, fmt.Errorf("missing auth config %q", authInfoName)
 	}
-
 	var tlsConfig *promauth.TLSConfig
 	var basicAuth *promauth.BasicAuthConfig
 	var token, tokenFile string
-	isHTTPS := strings.HasPrefix(configClusterInfo.Server, "https://")
-
-	if isHTTPS {
-		tlsConfig = &promauth.TLSConfig{
-			CAFile:             configClusterInfo.CertificateAuthority,
-			ServerName:         configClusterInfo.TLSServerName,
-			InsecureSkipVerify: configClusterInfo.InsecureSkipTLSVerify,
-		}
-	}
-
-	if len(configClusterInfo.CertificateAuthorityData) > 0 && isHTTPS {
-		tlsConfig.CA, err = base64.StdEncoding.DecodeString(configClusterInfo.CertificateAuthorityData)
-		if err != nil {
-			return nil, fmt.Errorf("cannot base64-decode configClusterInfo.CertificateAuthorityData %q: %w", configClusterInfo.CertificateAuthorityData, err)
-		}
-	}
-
 	if configAuthInfo != nil {
 		if err := configAuthInfo.validate(); err != nil {
-			return nil, fmt.Errorf("invalid user auth configuration for context: %s, err: %w", contextName, err)
+			return nil, fmt.Errorf("invalid auth config %q: %w", authInfoName, err)
 		}
-		if isHTTPS {
+		if strings.HasPrefix(configClusterInfo.Server, "https://") {
+			tlsConfig = &promauth.TLSConfig{
+				CAFile:             configClusterInfo.CertificateAuthority,
+				ServerName:         configClusterInfo.TLSServerName,
+				InsecureSkipVerify: configClusterInfo.InsecureSkipTLSVerify,
+			}
+			if len(configClusterInfo.CertificateAuthorityData) > 0 {
+				ca, err := base64.StdEncoding.DecodeString(configClusterInfo.CertificateAuthorityData)
+				if err != nil {
+					return nil, fmt.Errorf("cannot base64-decode certificate-authority-data from config %q at context %q: %w", clusterInfoName, contextName, err)
+				}
+				tlsConfig.CA = ca
+			}
 			tlsConfig.CertFile = configAuthInfo.ClientCertificate
 			tlsConfig.KeyFile = configAuthInfo.ClientKey
 
 			if len(configAuthInfo.ClientCertificateData) > 0 {
-				tlsConfig.Cert, err = base64.StdEncoding.DecodeString(configAuthInfo.ClientCertificateData)
+				cert, err := base64.StdEncoding.DecodeString(configAuthInfo.ClientCertificateData)
 				if err != nil {
-					return nil, fmt.Errorf("cannot base64-decode configAuthInfo.ClientCertificateData %q: %w", configClusterInfo.CertificateAuthorityData, err)
+					return nil, fmt.Errorf("cannot base64-decode client-certificate-data from %q: %w", authInfoName, err)
 				}
+				tlsConfig.Cert = cert
 			}
 			if len(configAuthInfo.ClientKeyData) > 0 {
-				tlsConfig.Key, err = base64.StdEncoding.DecodeString(configAuthInfo.ClientKeyData)
+				key, err := base64.StdEncoding.DecodeString(configAuthInfo.ClientKeyData)
 				if err != nil {
-					return nil, fmt.Errorf("cannot base64-decode configAuthInfo.ClientKeyData %q: %w", configClusterInfo.CertificateAuthorityData, err)
+					return nil, fmt.Errorf("cannot base64-decode client-key-data from %q: %w", authInfoName, err)
 				}
+				tlsConfig.Key = key
 			}
 		}
-
 		if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
 			basicAuth = &promauth.BasicAuthConfig{
 				Username: configAuthInfo.Username,
@@ -247,15 +252,13 @@ func buildConfig(sdc *SDConfig) (*kubeConfig, error) {
 		token = configAuthInfo.Token
 		tokenFile = configAuthInfo.TokenFile
 	}
-
-	kc := kubeConfig{
+	kc := &kubeConfig{
 		basicAuth: basicAuth,
-		server:    configClusterInfo.Server,
+		server:    server,
 		token:     token,
 		tokenFile: tokenFile,
 		tlsConfig: tlsConfig,
 		proxyURL:  configClusterInfo.ProxyURL,
 	}
-
-	return &kc, nil
+	return kc, nil
 }
diff --git a/lib/promscrape/discovery/kubernetes/kubernetes.go b/lib/promscrape/discovery/kubernetes/kubernetes.go
index d5ed48f3a..5652ec800 100644
--- a/lib/promscrape/discovery/kubernetes/kubernetes.go
+++ b/lib/promscrape/discovery/kubernetes/kubernetes.go
@@ -22,8 +22,9 @@ type SDConfig struct {
 
 	// Use role() function for accessing the Role field
 	Role string `yaml:"role"`
-	// if defined any cluster connection information from HTTPClientConfig will be ignored
-	KubeConfig string `yaml:"kubeconfig_file"`
+	// The filepath to kube config.
+	// If defined any cluster connection information from HTTPClientConfig is ignored.
+	KubeConfigFile string `yaml:"kubeconfig_file"`
 
 	HTTPClientConfig promauth.HTTPClientConfig `yaml:",inline"`
 	ProxyURL         *proxy.URL                `yaml:"proxy_url,omitempty"`
diff --git a/lib/promscrape/scrapework.go b/lib/promscrape/scrapework.go
index 2c4b7827c..92dda54d6 100644
--- a/lib/promscrape/scrapework.go
+++ b/lib/promscrape/scrapework.go
@@ -282,11 +282,16 @@ func (sw *scrapeWork) run(stopCh <-chan struct{}, globalStopCh <-chan struct{})
 		// This also makes consistent scrape times across restarts
 		// for a target with the same ScrapeURL and labels.
 		//
-		// Include clusterMemberNum to the key in order to guarantee that each member in vmagent cluster
+		// Include clusterName to the key in order to guarantee that the same
+		// scrape target is scraped at different offsets per each cluster.
+		// This guarantees that the deduplication consistently leaves samples received from the same vmagent.
+		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2679
+		//
+		// Include clusterMemberID to the key in order to guarantee that each member in vmagent cluster
 		// scrapes replicated targets at different time offsets. This guarantees that the deduplication consistently leaves samples
 		// received from the same vmagent replica.
 		// See https://docs.victoriametrics.com/vmagent.html#scraping-big-number-of-targets
-		key := fmt.Sprintf("ClusterMemberNum=%d, ScrapeURL=%s, Labels=%s", clusterMemberID, sw.Config.ScrapeURL, sw.Config.LabelsString())
+		key := fmt.Sprintf("clusterName=%s, clusterMemberID=%d, ScrapeURL=%s, Labels=%s", *clusterName, clusterMemberID, sw.Config.ScrapeURL, sw.Config.LabelsString())
 		h := xxhash.Sum64(bytesutil.ToUnsafeBytes(key))
 		randSleep = uint64(float64(scrapeInterval) * (float64(h) / (1 << 64)))
 		sleepOffset := uint64(time.Now().UnixNano()) % uint64(scrapeInterval)
@@ -495,7 +500,7 @@ func (sw *scrapeWork) scrapeInternal(scrapeTimestamp, realTimestamp int64) error
 		// This should reduce memory usage when scraping targets which return big responses.
 		leveledbytebufferpool.Put(body)
 	}
-	tsmGlobal.Update(sw, sw.ScrapeGroup, up == 1, realTimestamp, int64(duration*1000), samplesScraped, err)
+	tsmGlobal.Update(sw, up == 1, realTimestamp, int64(duration*1000), samplesScraped, err)
 	return err
 }
 
@@ -598,7 +603,7 @@ func (sw *scrapeWork) scrapeStream(scrapeTimestamp, realTimestamp int64) error {
 		sw.storeLastScrape(sbr.body)
 	}
 	sw.finalizeLastScrape()
-	tsmGlobal.Update(sw, sw.ScrapeGroup, up == 1, realTimestamp, int64(duration*1000), samplesScraped, err)
+	tsmGlobal.Update(sw, up == 1, realTimestamp, int64(duration*1000), samplesScraped, err)
 	// Do not track active series in streaming mode, since this may need too big amounts of memory
 	// when the target exports too big number of metrics.
 	return err
diff --git a/lib/promscrape/targetstatus.go b/lib/promscrape/targetstatus.go
index 66288abe9..e39f15b0f 100644
--- a/lib/promscrape/targetstatus.go
+++ b/lib/promscrape/targetstatus.go
@@ -16,7 +16,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
-	xxhash "github.com/cespare/xxhash/v2"
+	"github.com/cespare/xxhash/v2"
 )
 
 var maxDroppedTargets = flag.Int("promscrape.maxDroppedTargets", 1000, "The maximum number of droppedTargets to show at /api/v1/targets page. "+
@@ -45,19 +45,25 @@ func WriteTargetResponse(w http.ResponseWriter, r *http.Request) error {
 
 // WriteHumanReadableTargetsStatus writes human-readable status for all the scrape targets to w according to r.
 func WriteHumanReadableTargetsStatus(w http.ResponseWriter, r *http.Request) {
-	showOriginalLabels, _ := strconv.ParseBool(r.FormValue("show_original_labels"))
-	showOnlyUnhealthy, _ := strconv.ParseBool(r.FormValue("show_only_unhealthy"))
-	endpointSearch := strings.TrimSpace(r.FormValue("endpoint_search"))
-	labelSearch := strings.TrimSpace(r.FormValue("label_search"))
+	filter := getRequestFilter(r)
+	tsr := tsmGlobal.getTargetsStatusByJob(filter)
 	if accept := r.Header.Get("Accept"); strings.Contains(accept, "text/html") {
 		w.Header().Set("Content-Type", "text/html; charset=utf-8")
-		tsmGlobal.WriteTargetsHTML(w, showOnlyUnhealthy, endpointSearch, labelSearch)
+		WriteTargetsResponseHTML(w, tsr, filter)
 	} else {
 		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-		tsmGlobal.WriteTargetsPlain(w, showOriginalLabels, showOnlyUnhealthy, endpointSearch, labelSearch)
+		WriteTargetsResponsePlain(w, tsr, filter)
 	}
 }
 
+// WriteServiceDiscovery writes /service-discovery response to w similar to http://demo.robustperception.io:9090/service-discovery
+func WriteServiceDiscovery(w http.ResponseWriter, r *http.Request) {
+	filter := getRequestFilter(r)
+	tsr := tsmGlobal.getTargetsStatusByJob(filter)
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+	WriteServiceDiscoveryResponse(w, tsr, filter)
+}
+
 // WriteAPIV1Targets writes /api/v1/targets to w according to https://prometheus.io/docs/prometheus/latest/querying/api/#targets
 func WriteAPIV1Targets(w io.Writer, state string) {
 	if state == "" {
@@ -116,7 +122,7 @@ func (tsm *targetStatusMap) Unregister(sw *scrapeWork) {
 	tsm.mu.Unlock()
 }
 
-func (tsm *targetStatusMap) Update(sw *scrapeWork, group string, up bool, scrapeTime, scrapeDuration int64, samplesScraped int, err error) {
+func (tsm *targetStatusMap) Update(sw *scrapeWork, up bool, scrapeTime, scrapeDuration int64, samplesScraped int, err error) {
 	tsm.mu.Lock()
 	ts := tsm.m[sw]
 	if ts == nil {
@@ -126,7 +132,6 @@ func (tsm *targetStatusMap) Update(sw *scrapeWork, group string, up bool, scrape
 		tsm.m[sw] = ts
 	}
 	ts.up = up
-	ts.scrapeGroup = group
 	ts.scrapeTime = scrapeTime
 	ts.scrapeDuration = scrapeDuration
 	ts.samplesScraped = samplesScraped
@@ -158,8 +163,8 @@ func getTargetID(sw *scrapeWork) string {
 func (tsm *targetStatusMap) StatusByGroup(group string, up bool) int {
 	var count int
 	tsm.mu.Lock()
-	for _, st := range tsm.m {
-		if st.scrapeGroup == group && st.up == up {
+	for _, ts := range tsm.m {
+		if ts.sw.ScrapeGroup == group && ts.up == up {
 			count++
 		}
 	}
@@ -167,50 +172,48 @@ func (tsm *targetStatusMap) StatusByGroup(group string, up bool) int {
 	return count
 }
 
-// WriteActiveTargetsJSON writes `activeTargets` contents to w according to https://prometheus.io/docs/prometheus/latest/querying/api/#targets
-func (tsm *targetStatusMap) WriteActiveTargetsJSON(w io.Writer) {
+func (tsm *targetStatusMap) getActiveTargetStatuses() []targetStatus {
 	tsm.mu.Lock()
-	type keyStatus struct {
-		key string
-		st  targetStatus
-	}
-	kss := make([]keyStatus, 0, len(tsm.m))
-	for sw, st := range tsm.m {
-		key := promLabelsString(sw.Config.OriginalLabels)
-		kss = append(kss, keyStatus{
-			key: key,
-			st:  *st,
-		})
+	tss := make([]targetStatus, 0, len(tsm.m))
+	for _, ts := range tsm.m {
+		tss = append(tss, *ts)
 	}
 	tsm.mu.Unlock()
-
-	sort.Slice(kss, func(i, j int) bool {
-		return kss[i].key < kss[j].key
+	// Sort discovered targets by __address__ label, so they stay in consistent order across calls
+	sort.Slice(tss, func(i, j int) bool {
+		addr1 := promrelabel.GetLabelValueByName(tss[i].sw.Config.OriginalLabels, "__address__")
+		addr2 := promrelabel.GetLabelValueByName(tss[j].sw.Config.OriginalLabels, "__address__")
+		return addr1 < addr2
 	})
+	return tss
+}
+
+// WriteActiveTargetsJSON writes `activeTargets` contents to w according to https://prometheus.io/docs/prometheus/latest/querying/api/#targets
+func (tsm *targetStatusMap) WriteActiveTargetsJSON(w io.Writer) {
+	tss := tsm.getActiveTargetStatuses()
 	fmt.Fprintf(w, `[`)
-	for i, ks := range kss {
-		st := ks.st
+	for i, ts := range tss {
 		fmt.Fprintf(w, `{"discoveredLabels":`)
-		writeLabelsJSON(w, st.sw.Config.OriginalLabels)
+		writeLabelsJSON(w, ts.sw.Config.OriginalLabels)
 		fmt.Fprintf(w, `,"labels":`)
-		labelsFinalized := promrelabel.FinalizeLabels(nil, st.sw.Config.Labels)
+		labelsFinalized := promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels)
 		writeLabelsJSON(w, labelsFinalized)
-		fmt.Fprintf(w, `,"scrapePool":%q`, st.sw.Config.Job())
-		fmt.Fprintf(w, `,"scrapeUrl":%q`, st.sw.Config.ScrapeURL)
+		fmt.Fprintf(w, `,"scrapePool":%q`, ts.sw.Config.Job())
+		fmt.Fprintf(w, `,"scrapeUrl":%q`, ts.sw.Config.ScrapeURL)
 		errMsg := ""
-		if st.err != nil {
-			errMsg = st.err.Error()
+		if ts.err != nil {
+			errMsg = ts.err.Error()
 		}
 		fmt.Fprintf(w, `,"lastError":%q`, errMsg)
-		fmt.Fprintf(w, `,"lastScrape":%q`, time.Unix(st.scrapeTime/1000, (st.scrapeTime%1000)*1e6).Format(time.RFC3339Nano))
-		fmt.Fprintf(w, `,"lastScrapeDuration":%g`, (time.Millisecond * time.Duration(st.scrapeDuration)).Seconds())
-		fmt.Fprintf(w, `,"lastSamplesScraped":%d`, st.samplesScraped)
+		fmt.Fprintf(w, `,"lastScrape":%q`, time.Unix(ts.scrapeTime/1000, (ts.scrapeTime%1000)*1e6).Format(time.RFC3339Nano))
+		fmt.Fprintf(w, `,"lastScrapeDuration":%g`, (time.Millisecond * time.Duration(ts.scrapeDuration)).Seconds())
+		fmt.Fprintf(w, `,"lastSamplesScraped":%d`, ts.samplesScraped)
 		state := "up"
-		if !st.up {
+		if !ts.up {
 			state = "down"
 		}
 		fmt.Fprintf(w, `,"health":%q}`, state)
-		if i+1 < len(kss) {
+		if i+1 < len(tss) {
 			fmt.Fprintf(w, `,`)
 		}
 	}
@@ -231,7 +234,6 @@ func writeLabelsJSON(w io.Writer, labels []prompbmarshal.Label) {
 type targetStatus struct {
 	sw             *scrapeWork
 	up             bool
-	scrapeGroup    string
 	scrapeTime     int64
 	scrapeDuration int64
 	samplesScraped int
@@ -240,8 +242,8 @@ type targetStatus struct {
 	err            error
 }
 
-func (st *targetStatus) getDurationFromLastScrape() time.Duration {
-	return time.Since(time.Unix(st.scrapeTime/1000, (st.scrapeTime%1000)*1e6))
+func (ts *targetStatus) getDurationFromLastScrape() time.Duration {
+	return time.Since(time.Unix(ts.scrapeTime/1000, (ts.scrapeTime%1000)*1e6))
 }
 
 type droppedTargets struct {
@@ -255,6 +257,22 @@ type droppedTarget struct {
 	deadline       uint64
 }
 
+func (dt *droppedTargets) getTargetsLabels() [][]prompbmarshal.Label {
+	dt.mu.Lock()
+	dtls := make([][]prompbmarshal.Label, 0, len(dt.m))
+	for _, v := range dt.m {
+		dtls = append(dtls, v.originalLabels)
+	}
+	dt.mu.Unlock()
+	// Sort discovered targets by __address__ label, so they stay in consistent order across calls
+	sort.Slice(dtls, func(i, j int) bool {
+		addr1 := promrelabel.GetLabelValueByName(dtls[i], "__address__")
+		addr2 := promrelabel.GetLabelValueByName(dtls[j], "__address__")
+		return addr1 < addr2
+	})
+	return dtls
+}
+
 func (dt *droppedTargets) Register(originalLabels []prompbmarshal.Label) {
 	// It is better to have hash collisions instead of spending additional CPU on promLabelsString() call.
 	key := labelsHash(originalLabels)
@@ -300,30 +318,13 @@ var xxhashPool = &sync.Pool{
 
 // WriteDroppedTargetsJSON writes `droppedTargets` contents to w according to https://prometheus.io/docs/prometheus/latest/querying/api/#targets
 func (dt *droppedTargets) WriteDroppedTargetsJSON(w io.Writer) {
-	dt.mu.Lock()
-	type keyStatus struct {
-		key            string
-		originalLabels []prompbmarshal.Label
-	}
-	kss := make([]keyStatus, 0, len(dt.m))
-	for _, v := range dt.m {
-		key := promLabelsString(v.originalLabels)
-		kss = append(kss, keyStatus{
-			key:            key,
-			originalLabels: v.originalLabels,
-		})
-	}
-	dt.mu.Unlock()
-
-	sort.Slice(kss, func(i, j int) bool {
-		return kss[i].key < kss[j].key
-	})
+	dtls := dt.getTargetsLabels()
 	fmt.Fprintf(w, `[`)
-	for i, ks := range kss {
+	for i, labels := range dtls {
 		fmt.Fprintf(w, `{"discoveredLabels":`)
-		writeLabelsJSON(w, ks.originalLabels)
+		writeLabelsJSON(w, labels)
 		fmt.Fprintf(w, `}`)
-		if i+1 < len(kss) {
+		if i+1 < len(dtls) {
 			fmt.Fprintf(w, `,`)
 		}
 	}
@@ -335,24 +336,24 @@ var droppedTargetsMap = &droppedTargets{
 }
 
 type jobTargetsStatuses struct {
-	job           string
+	jobName       string
 	upCount       int
 	targetsTotal  int
 	targetsStatus []targetStatus
 }
 
-func (tsm *targetStatusMap) getTargetsStatusByJob(endpointSearch, labelSearch string) ([]jobTargetsStatuses, []string, error) {
+func (tsm *targetStatusMap) getTargetsStatusByJob(filter *requestFilter) *targetsStatusResult {
 	byJob := make(map[string][]targetStatus)
 	tsm.mu.Lock()
-	for _, st := range tsm.m {
-		job := st.sw.Config.jobNameOriginal
-		byJob[job] = append(byJob[job], *st)
+	for _, ts := range tsm.m {
+		jobName := ts.sw.Config.jobNameOriginal
+		byJob[jobName] = append(byJob[jobName], *ts)
 	}
 	jobNames := append([]string{}, tsm.jobNames...)
 	tsm.mu.Unlock()
 
-	var jts []jobTargetsStatuses
-	for job, statuses := range byJob {
+	var jts []*jobTargetsStatuses
+	for jobName, statuses := range byJob {
 		sort.Slice(statuses, func(i, j int) bool {
 			return statuses[i].sw.Config.ScrapeURL < statuses[j].sw.Config.ScrapeURL
 		})
@@ -362,29 +363,38 @@ func (tsm *targetStatusMap) getTargetsStatusByJob(endpointSearch, labelSearch st
 			if ts.up {
 				ups++
 			}
+			if filter.showOnlyUnhealthy && ts.up {
+				continue
+			}
 			targetsStatuses = append(targetsStatuses, ts)
 		}
-		jts = append(jts, jobTargetsStatuses{
-			job:           job,
+		jts = append(jts, &jobTargetsStatuses{
+			jobName:       jobName,
 			upCount:       ups,
 			targetsTotal:  len(statuses),
 			targetsStatus: targetsStatuses,
 		})
 	}
 	sort.Slice(jts, func(i, j int) bool {
-		return jts[i].job < jts[j].job
+		return jts[i].jobName < jts[j].jobName
 	})
 	emptyJobs := getEmptyJobs(jts, jobNames)
 	var err error
-	jts, err = filterTargets(jts, endpointSearch, labelSearch)
-	if len(endpointSearch) > 0 || len(labelSearch) > 0 {
+	jts, err = filterTargets(jts, filter.endpointSearch, filter.labelSearch)
+	if len(filter.endpointSearch) > 0 || len(filter.labelSearch) > 0 {
 		// Do not show empty jobs if target filters are set.
 		emptyJobs = nil
 	}
-	return jts, emptyJobs, err
+	dtls := droppedTargetsMap.getTargetsLabels()
+	return &targetsStatusResult{
+		jobTargetsStatuses:   jts,
+		droppedTargetsLabels: dtls,
+		emptyJobs:            emptyJobs,
+		err:                  err,
+	}
 }
 
-func filterTargetsByEndpoint(jts []jobTargetsStatuses, searchQuery string) ([]jobTargetsStatuses, error) {
+func filterTargetsByEndpoint(jts []*jobTargetsStatuses, searchQuery string) ([]*jobTargetsStatuses, error) {
 	if searchQuery == "" {
 		return jts, nil
 	}
@@ -392,7 +402,7 @@ func filterTargetsByEndpoint(jts []jobTargetsStatuses, searchQuery string) ([]jo
 	if err != nil {
 		return nil, fmt.Errorf("cannot parse %s: %w", searchQuery, err)
 	}
-	var jtsFiltered []jobTargetsStatuses
+	var jtsFiltered []*jobTargetsStatuses
 	for _, job := range jts {
 		var tss []targetStatus
 		for _, ts := range job.targetsStatus {
@@ -410,7 +420,7 @@ func filterTargetsByEndpoint(jts []jobTargetsStatuses, searchQuery string) ([]jo
 	return jtsFiltered, nil
 }
 
-func filterTargetsByLabels(jts []jobTargetsStatuses, searchQuery string) ([]jobTargetsStatuses, error) {
+func filterTargetsByLabels(jts []*jobTargetsStatuses, searchQuery string) ([]*jobTargetsStatuses, error) {
 	if searchQuery == "" {
 		return jts, nil
 	}
@@ -418,7 +428,7 @@ func filterTargetsByLabels(jts []jobTargetsStatuses, searchQuery string) ([]jobT
 	if err := ie.Parse(searchQuery); err != nil {
 		return nil, fmt.Errorf("cannot parse %s: %w", searchQuery, err)
 	}
-	var jtsFiltered []jobTargetsStatuses
+	var jtsFiltered []*jobTargetsStatuses
 	for _, job := range jts {
 		var tss []targetStatus
 		for _, ts := range job.targetsStatus {
@@ -436,7 +446,7 @@ func filterTargetsByLabels(jts []jobTargetsStatuses, searchQuery string) ([]jobT
 	return jtsFiltered, nil
 }
 
-func filterTargets(jts []jobTargetsStatuses, endpointQuery, labelQuery string) ([]jobTargetsStatuses, error) {
+func filterTargets(jts []*jobTargetsStatuses, endpointQuery, labelQuery string) ([]*jobTargetsStatuses, error) {
 	var err error
 	jts, err = filterTargetsByEndpoint(jts, endpointQuery)
 	if err != nil {
@@ -449,13 +459,13 @@ func filterTargets(jts []jobTargetsStatuses, endpointQuery, labelQuery string) (
 	return jts, nil
 }
 
-func getEmptyJobs(jts []jobTargetsStatuses, jobNames []string) []string {
+func getEmptyJobs(jts []*jobTargetsStatuses, jobNames []string) []string {
 	jobNamesMap := make(map[string]struct{}, len(jobNames))
 	for _, jobName := range jobNames {
 		jobNamesMap[jobName] = struct{}{}
 	}
 	for i := range jts {
-		delete(jobNamesMap, jts[i].job)
+		delete(jobNamesMap, jts[i].jobName)
 	}
 	emptyJobs := make([]string, 0, len(jobNamesMap))
 	for k := range jobNamesMap {
@@ -465,16 +475,85 @@ func getEmptyJobs(jts []jobTargetsStatuses, jobNames []string) []string {
 	return emptyJobs
 }
 
-// WriteTargetsHTML writes targets status grouped by job into writer w in html table,
-// accepts filter to show only unhealthy targets.
-func (tsm *targetStatusMap) WriteTargetsHTML(w io.Writer, showOnlyUnhealthy bool, endpointSearch, labelSearch string) {
-	jss, emptyJobs, err := tsm.getTargetsStatusByJob(endpointSearch, labelSearch)
-	WriteTargetsResponseHTML(w, jss, emptyJobs, showOnlyUnhealthy, endpointSearch, labelSearch, err)
+type requestFilter struct {
+	showOriginalLabels bool
+	showOnlyUnhealthy  bool
+	endpointSearch     string
+	labelSearch        string
 }
 
-// WriteTargetsPlain writes targets grouped by job into writer w in plain text,
-// accept filter to show original labels.
-func (tsm *targetStatusMap) WriteTargetsPlain(w io.Writer, showOriginalLabels, showOnlyUnhealthy bool, endpointSearch, labelSearch string) {
-	jss, emptyJobs, err := tsm.getTargetsStatusByJob(endpointSearch, labelSearch)
-	WriteTargetsResponsePlain(w, jss, emptyJobs, showOriginalLabels, showOnlyUnhealthy, err)
+func getRequestFilter(r *http.Request) *requestFilter {
+	showOriginalLabels, _ := strconv.ParseBool(r.FormValue("show_original_labels"))
+	showOnlyUnhealthy, _ := strconv.ParseBool(r.FormValue("show_only_unhealthy"))
+	endpointSearch := strings.TrimSpace(r.FormValue("endpoint_search"))
+	labelSearch := strings.TrimSpace(r.FormValue("label_search"))
+	return &requestFilter{
+		showOriginalLabels: showOriginalLabels,
+		showOnlyUnhealthy:  showOnlyUnhealthy,
+		endpointSearch:     endpointSearch,
+		labelSearch:        labelSearch,
+	}
+}
+
+type targetsStatusResult struct {
+	jobTargetsStatuses   []*jobTargetsStatuses
+	droppedTargetsLabels [][]prompbmarshal.Label
+	emptyJobs            []string
+	err                  error
+}
+
+type targetLabels struct {
+	up               bool
+	discoveredLabels []prompbmarshal.Label
+	labels           []prompbmarshal.Label
+}
+type targetLabelsByJob struct {
+	jobName        string
+	targets        []targetLabels
+	activeTargets  int
+	droppedTargets int
+}
+
+func (tsr *targetsStatusResult) getTargetLabelsByJob() []*targetLabelsByJob {
+	byJob := make(map[string]*targetLabelsByJob)
+	for _, jts := range tsr.jobTargetsStatuses {
+		jobName := jts.jobName
+		for _, ts := range jts.targetsStatus {
+			m := byJob[jobName]
+			if m == nil {
+				m = &targetLabelsByJob{
+					jobName: jobName,
+				}
+				byJob[jobName] = m
+			}
+			m.activeTargets++
+			m.targets = append(m.targets, targetLabels{
+				up:               ts.up,
+				discoveredLabels: ts.sw.Config.OriginalLabels,
+				labels:           ts.sw.Config.Labels,
+			})
+		}
+	}
+	for _, labels := range tsr.droppedTargetsLabels {
+		jobName := promrelabel.GetLabelValueByName(labels, "job")
+		m := byJob[jobName]
+		if m == nil {
+			m = &targetLabelsByJob{
+				jobName: jobName,
+			}
+			byJob[jobName] = m
+		}
+		m.droppedTargets++
+		m.targets = append(m.targets, targetLabels{
+			discoveredLabels: labels,
+		})
+	}
+	a := make([]*targetLabelsByJob, 0, len(byJob))
+	for _, tls := range byJob {
+		a = append(a, tls)
+	}
+	sort.Slice(a, func(i, j int) bool {
+		return a[i].jobName < a[j].jobName
+	})
+	return a
 }
diff --git a/lib/promscrape/targetstatus.qtpl b/lib/promscrape/targetstatus.qtpl
index 355c9b2b6..5bfaba60e 100644
--- a/lib/promscrape/targetstatus.qtpl
+++ b/lib/promscrape/targetstatus.qtpl
@@ -1,33 +1,32 @@
 {% import (
 	"net/url"
 	"time"
+
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
 ) %}
 
 {% stripspace %}
 
-{% func TargetsResponsePlain(jts []jobTargetsStatuses, emptyJobs []string, showOriginLabels, showOnlyUnhealthy bool, err error) %}
+{% func TargetsResponsePlain(tsr *targetsStatusResult, filter *requestFilter) %}
 
-{% if err != nil %}
-	{%s= err.Error() %}
+{% if tsr.err != nil %}
+	{%s= tsr.err.Error() %}
 	{% return %}
 {% endif %}
 
-{% for _, js := range jts %}
-	{% if showOnlyUnhealthy && js.upCount == js.targetsTotal %}{% continue %}{% endif %}
-	job={%q= js.job %} ({%d js.upCount %}/{%d js.targetsTotal %}{% space %}up)
+{% for _, jts := range tsr.jobTargetsStatuses %}
+	job={%s= jts.jobName %}{% space %}({%d jts.upCount %}/{%d jts.targetsTotal %}{% space %}up)
 	{% newline %}
-	{% for _, ts := range js.targetsStatus %}
-		{% if showOnlyUnhealthy && ts.up %}{% continue %}{% endif %}
+	{% for _, ts := range jts.targetsStatus %}
 		{%s= "\t" %}
 		state={% if ts.up %}up{% else %}down{% endif %},{% space %}
 		endpoint={%s= ts.sw.Config.ScrapeURL %},{% space %}
 		labels={%s= promLabelsString(promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels)) %},{% space %}
-		{% if showOriginLabels %}originalLabels={%s= promLabelsString(ts.sw.Config.OriginalLabels) %},{% space %}{% endif %}
+		{% if filter.showOriginalLabels %}originalLabels={%s= promLabelsString(ts.sw.Config.OriginalLabels) %},{% space %}{% endif %}
 		scrapes_total={%d ts.scrapesTotal %},{% space %}
 		scrapes_failed={%d ts.scrapesFailed %},{% space %}
-		last_scrape={%f.3 ts.getDurationFromLastScrape().Seconds() %}s ago,{% space %}
+		last_scrape={%d int(ts.getDurationFromLastScrape().Milliseconds()) %}ms ago,{% space %}
 		scrape_duration={%d int(ts.scrapeDuration) %}ms,{% space %}
 		samples_scraped={%d ts.samplesScraped %},{% space %}
 		error={% if ts.err != nil %}{%s= ts.err.Error() %}{% endif %}
@@ -35,43 +34,74 @@
 	{% endfor %}
 {% endfor %}
 
-{% for _, jobName := range emptyJobs %}
-job={%q= jobName %} (0/0 up)
+{% for _, jobName := range tsr.emptyJobs %}
+	job={%s= jobName %}{% space %}(0/0 up)
 {% newline %}
 {% endfor %}
 
 {% endfunc %}
 
-{% func TargetsResponseHTML(jts []jobTargetsStatuses, emptyJobs []string, showOnlyUnhealthy bool, endpointSearch, labelSearch string, err error) %}
+{% func TargetsResponseHTML(tsr *targetsStatusResult, filter *requestFilter) %}
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link href="static/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
-    <title>Scrape targets</title>
-    <script>
-function collapse_all() {
-  for (var i = 0; i < {%d len(jts) %}; i++) {
-    let el = document.getElementById("table-" + i);
-    if (!el) {
-        continue;
-    }
-    el.style.display = 'none';
-  }
-}
-function expand_all() {
-  for (var i = 0; i < {%d len(jts) %}; i++) {
-    let el = document.getElementById("table-" + i);
-    if (!el) {
-      continue;
-    }
-    el.style.display = 'block';
-  }
-}
-    </script>
+    {%= commonHeader() %}
+    <title>Active Targets</title>
+</head>
+<body>
+    {%= navbar() %}
+    <div class="container-fluid">
+        {% if tsr.err != nil %}
+            {%= errorNotification(tsr.err) %}
+        {% endif %}
+        <div class="row">
+            <main class="col-12">
+                <h1>Active Targets</h1>
+                <hr />
+                {%= filtersForm(filter) %}
+                <hr />
+                {%= targetsTabs(tsr, filter, "scrapeTargets") %}
+            </main>
+        </div>
+    </div>
+</body>
+</html>
+{% endfunc %}
+
+{% func ServiceDiscoveryResponse(tsr *targetsStatusResult, filter *requestFilter) %}
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    {%= commonHeader() %}
+    <title>Discovered Targets</title>
 </head>
 <body>
+    {%= navbar() %}
+    <div class="container-fluid">
+        {% if tsr.err != nil %}
+            {%= errorNotification(tsr.err) %}
+        {% endif %}
+        <div class="row">
+            <main class="col-12">
+                <h1>Discovered Targets</h1>
+                <hr />
+                {%= filtersForm(filter) %}
+                <hr />
+		{%= targetsTabs(tsr, filter, "discoveredTargets") %}
+            </main>
+        </div>
+    </div>
+</body>
+</html>
+{% endfunc %}
+
+{% func commonHeader() %}
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link href="static/css/bootstrap.min.css" rel="stylesheet" />
+{% endfunc %}
+
+{% func navbar() %}
     <div class="navbar navbar-dark bg-dark box-shadow">
         <div class="d-flex justify-content-between">
             <a href="#" class="navbar-brand d-flex align-items-center ms-3" title="The High Performance Open Source Time Series Database &amp; Monitoring Solution ">
@@ -80,181 +110,264 @@ function expand_all() {
             </a>
         </div>
     </div>
-    <div class="container-fluid">
-        {% if err != nil %}
-            {%= errorNotification(err) %}
-        {% endif %}
-        <div class="row">
-            <main class="col-12">
-                <h1>Scrape targets</h1>
-                <hr />
-                <div class="row g-3 align-items-center mb-3">
-                    <div class="col-auto">
-                        <button type="button" class="btn{% space %}{% if !showOnlyUnhealthy %}btn-secondary{% else %}btn-success{% endif %}" onclick="location.href='?{%= queryArgs(map[string]string{
-	"show_only_unhealthy": "false",
-	"endpoint_search": endpointSearch,
-	"label_search": labelSearch,
-}) %}'">
-                            All
-                        </button>
-                    </div>
-                    <div class="col-auto">
-                        <button type="button" class="btn{% space %}{% if showOnlyUnhealthy %}btn-secondary{% else %}btn-danger{% endif %}" onclick="location.href='?{%= queryArgs(map[string]string{
-	"show_only_unhealthy": "true",
-	"endpoint_search": endpointSearch,
-	"label_search": labelSearch,
-}) %}'">
-                            Unhealthy
-                        </button>
-                    </div>
-                    <div class="col-auto">
-                        <button type="button" class="btn btn-primary" onclick="collapse_all()">
-                            Collapse all
-                        </button>
-                    </div>
-                    <div class="col-auto">
-                        <button type="button" class="btn btn-secondary" onclick="expand_all()">
-                            Expand all
-                        </button>
-                    </div>
-                    <div class="col-auto">
-                    {% if endpointSearch == "" && labelSearch == "" %}
-                        <button type="button" class="btn btn-success" onclick="document.getElementById('filters').style.display='block'">
-                            Filter targets
-                        </button>
-                    {% else %}
-                        <button type="button" class="btn btn-danger" onclick="location.href='?'">
-                            Clear target filters
-                        </button>
-                    {% endif %}
-                    </div>
-                </div>
-                <div id="filters" {% if endpointSearch == "" && labelSearch == "" %}style="display:none"{% endif %}>
-                    <form class="form-horizontal">
-                        <div class="form-group mb-3">
-                            <label for="endpoint_search" class="col-sm-10 control-label">Endpoint filter (<a target="_blank" href="https://github.com/google/re2/wiki/Syntax">Regexp</a> is accepted)</label>
-                            <div class="col-sm-10">
-                                <input type="text" id="endpoint_search" name="endpoint_search"
-                                    placeholder="For example, 127.0.0.1" class="form-control" value="{%s endpointSearch %}"/>
-                            </div>
-                        </div>
-                        <div class="form-group mb-3">
-                            <label for="label_search" class="col-sm-10 control-label">Labels filter (<a target="_blank" href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors">Arbitrary time series selectors</a> are accepted)</label>
-                            <div class="col-sm-10">
-                                <input type="text" id="label_search" name="label_search"
-                                    placeholder="For example, {instance=~'.+:9100'}" class="form-control" value="{%s labelSearch %}"/>
-                            </div>
-                        </div>
-                        <input type="hidden" name="show_only_unhealthy" value="{%v showOnlyUnhealthy %}"/>
-                        <button type="submit" class="btn btn-success mb-3">Submit</button>
-                    </form>
+{% endfunc %}
+
+{% func filtersForm(filter *requestFilter) %}
+    <div class="row g-3 align-items-center mb-3">
+        <div class="col-auto">
+            <button id="all-btn" type="button" class="btn{% space %}{% if !filter.showOnlyUnhealthy %}btn-secondary{% else %}btn-success{% endif %}"
+              onclick="location.href='?{%= queryArgs(filter, map[string]string{"show_only_unhealthy": "false"}) %}'">
+                All
+            </button>
+        </div>
+        <div class="col-auto">
+            <button id="unhealthy-btn" type="button" class="btn{% space %}{% if filter.showOnlyUnhealthy %}btn-secondary{% else %}btn-danger{% endif %}"
+              onclick="location.href='?{%= queryArgs(filter, map[string]string{"show_only_unhealthy": "true"}) %}'">
+                Unhealthy
+            </button>
+        </div>
+        <div class="col-auto">
+            <button type="button" class="btn btn-primary" onclick="document.querySelectorAll('.scrape-job').forEach((el) => { el.style.display = 'none'; })">
+                Collapse all
+            </button>
+        </div>
+        <div class="col-auto">
+            <button type="button" class="btn btn-secondary" onclick="document.querySelectorAll('.scrape-job').forEach((el) => { el.style.display = 'block'; })">
+                Expand all
+            </button>
+        </div>
+        <div class="col-auto">
+            <button type="button" class="btn btn-success" onclick="document.getElementById('filters').style.display='block'">
+                Filter targets
+            </button>
+        </div>
+    </div>
+    <div id="filters" {% if filter.endpointSearch == "" && filter.labelSearch == "" %}style="display:none"{% endif %}>
+        <form class="form-horizontal">
+            <div class="form-group mb-3">
+                <label for="endpoint_search" class="col-sm-10 control-label">Endpoint filter (<a target="_blank" href="https://github.com/google/re2/wiki/Syntax">Regexp</a> is accepted)</label>
+                <div class="col-sm-10">
+                    <input type="text" id="endpoint_search" name="endpoint_search"
+                      placeholder="For example, 127.0.0.1" class="form-control" value="{%s filter.endpointSearch %}"/>
                 </div>
-                <hr />
-                <div class="row">
-                    <div class="col-12">
-                        {% for i, js := range jts %}
-                        {% if showOnlyUnhealthy && js.upCount == js.targetsTotal %}{% continue %}{% endif %}
-                        <div class="row mb-4">
-                            <div class="col-12">
-                                <h4>
-                                    <span class="me-2">{%s js.job %}{% space %}({%d js.upCount %}/{%d js.targetsTotal %}{% space %}up)</span>
-                                    <button type="button" class="btn btn-primary btn-sm me-1"
-                                            onclick="document.getElementById('table-{%d i %}').style.display='none'">collapse
-                                    </button>
-                                    <button type="button" class="btn btn-secondary btn-sm me-1"
-                                            onclick="document.getElementById('table-{%d i %}').style.display='block'">expand
-                                    </button>
-                                </h4>
-                                <div id="table-{%d i %}" class="table-responsive">
-                                    <table class="table table-striped table-hover table-bordered table-sm">
-                                        <thead>
-                                        <tr>
-                                            <th scope="col">Endpoint</th>
-                                            <th scope="col">State</th>
-                                            <th scope="col" title="scrape target labels">Labels</th>
-                                            <th scope="col" title="total scrapes">Scrapes</th>
-                                            <th scope="col" title="total scrape errors">Errors</th>
-                                            <th scope="col" title="the time of the last scrape">Last Scrape</th>
-                                            <th scope="col" title="the duration of the last scrape">Duration</th>
-                                            <th scope="col" title="the number of metrics scraped during the last scrape">Samples</th>
-                                            <th scope="col" title="error from the last scrape (if any)">Last error</th>
-                                        </tr>
-                                        </thead>
-                                        <tbody class="list-{%d i %}">
-                                        {% for _, ts := range js.targetsStatus %}
-                                        {% code
-                                        endpoint := ts.sw.Config.ScrapeURL
-                                        targetID := getTargetID(ts.sw)
-                                        lastScrapeTime := ts.getDurationFromLastScrape()
-                                        %}
-                                        {% if showOnlyUnhealthy && ts.up %}{% continue %}{% endif %}
-                                        <tr {% if !ts.up %}{%space%}class="alert alert-danger" role="alert" {% endif %}>
-                                            <td class="endpoint"><a href="{%s endpoint %}" target="_blank">{%s endpoint %}</a> (
-                                                <a href="target_response?id={%s targetID %}" target="_blank"
-                                                   title="click to fetch target response on behalf of the scraper">response</a>
-                                                )
-                                            </td>
-                                            <td>{% if ts.up %}UP{% else %}DOWN{% endif %}</td>
-                                            <td class="labels">
-                                                <div title="click to show original labels"
-                                                     onclick="document.getElementById('original_labels_{%s targetID %}').style.display='block'">
-                                                    {%= formatLabel(promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels)) %}
-                                                </div>
-                                                <div style="display:none" id="original_labels_{%s targetID %}">
-                                                    {%= formatLabel(ts.sw.Config.OriginalLabels) %}
-                                                </div>
-                                            </td>
-                                            <td>{%d ts.scrapesTotal %}</td>
-                                            <td>{%d ts.scrapesFailed %}</td>
-                                            <td>
-                                                {% if lastScrapeTime < 365*24*time.Hour %}
-                                                {%f.3 lastScrapeTime.Seconds() %}s ago
-                                                {% else %}
-                                                none
-                                                {% endif %}
-                                            <td>{%d int(ts.scrapeDuration) %}ms</td>
-                                            <td>{%d ts.samplesScraped %}</td>
-                                            <td>{% if ts.err != nil %}{%s ts.err.Error() %}{% endif %}</td>
-                                        </tr>
-                                        {% endfor %}
-                                        </tbody>
-                                    </table>
-                                </div>
-                            </div>
-                        </div>
-                        {% endfor %}
-                    </div>
+            </div>
+            <div class="form-group mb-3">
+                <label for="label_search" class="col-sm-10 control-label">Labels filter (<a target="_blank" href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors">Arbitrary time series selectors</a> are accepted)</label>
+                <div class="col-sm-10">
+                    <input type="text" id="label_search" name="label_search"
+                      placeholder="For example, {instance=~'.+:9100'}" class="form-control" value="{%s filter.labelSearch %}"/>
                 </div>
+            </div>
+            <input type="hidden" name="show_only_unhealthy" value="{%v filter.showOnlyUnhealthy %}"/>
+            <input type="hidden" name="show_original_labels" value="{%v filter.showOriginalLabels %}"/>
+            <button type="submit" class="btn btn-success mb-3">Submit</button>
+            <button type="button" class="btn btn-danger mb-3" onclick="location.href='?'">Clear target filters</button>
+        </form>
+    </div>
+{% endfunc %}
+
+{% func targetsTabs(tsr *targetsStatusResult, filter *requestFilter, activeTab string) %}
+    <ul class="nav nav-tabs" id="myTab" role="tablist">
+        <li class="nav-item" role="presentation">
+            <button class="nav-link{%if activeTab=="scrapeTargets"%}{% space %}active{%endif%}" type="button" role="tab"
+              onclick="location.href='targets?{%= queryArgs(filter, nil) %}'">
+                Active targets
+            </button>
+        </li>
+        <li class="nav-item" role="presentation">
+            <button class="nav-link{%if activeTab=="discoveredTargets"%}{% space %}active{%endif%}" type="button" role="tab"
+              onclick="location.href='service-discovery?{%= queryArgs(filter, nil) %}'">
+                Discovered targets
+            </button>
+        </li>
+    </ul>
+    <div class="tab-content">
+        <div class="tab-pane active" role="tabpanel">
+            {% switch activeTab %}
+            {% case "scrapeTargets" %}
+                {%= scrapeTargets(tsr) %}
+            {% case "discoveredTargets" %}
+                {%= discoveredTargets(tsr) %}
+            {% endswitch %}
+        </div>
+    </div>
+{% endfunc %}
+
+{% func scrapeTargets(tsr *targetsStatusResult) %}
+    <div class="row mt-4">
+        <div class="col-12">
+            {% for i, jts := range tsr.jobTargetsStatuses %}
+                {%= scrapeJobTargets(i, jts) %}
+            {% endfor %}
+            {% for i, jobName := range tsr.emptyJobs %}
+                {% code
+                    num := i + len(tsr.jobTargetsStatuses)
+                    jts := &jobTargetsStatuses{
+                        jobName: jobName,
+                    }
+                %}
+                {%= scrapeJobTargets(num, jts) %}
+            {% endfor %}
+        </div>
+   </div>
+{% endfunc %}
 
-                  {% for _, jobName := range emptyJobs %}
-                    <div>
-                      <h4>
-                        <a>{%s jobName %} (0/0 up)</a>
-                      </h4>
-                      <table class="table table-striped table-hover table-bordered table-sm">
-                        <thead>
-                          <tr>
+{% func scrapeJobTargets(num int, jts *jobTargetsStatuses) %}
+    <div class="row mb-4">
+        <div class="col-12">
+            <h4>
+                <span class="me-2">{%s jts.jobName %}{% space %}({%d jts.upCount %}/{%d jts.targetsTotal %}{% space %}up)</span>
+                {%= showHideScrapeJobButtons(num) %}
+            </h4>
+            <div id="scrape-job-{%d num %}" class="scrape-job table-responsive">
+                <table class="table table-striped table-hover table-bordered table-sm">
+                    <thead>
+                        <tr>
                             <th scope="col">Endpoint</th>
                             <th scope="col">State</th>
-                            <th scope="col">Labels</th>
-                            <th scope="col">Last Scrape</th>
-                            <th scope="col">Scrape Duration</th>
-                            <th scope="col">Samples Scraped</th>
-                            <th scope="col">Error</th>
-                          </tr>
-                        </thead>
-                      </table>
-                    </div>
-                  {% endfor %}
-            </main>
+                            <th scope="col" title="target labels">Labels</th>
+                            <th scope="col" title="total scrapes">Scrapes</th>
+                            <th scope="col" title="total scrape errors">Errors</th>
+                            <th scope="col" title="the time of the last scrape">Last Scrape</th>
+                            <th scope="col" title="the duration of the last scrape">Duration</th>
+                            <th scope="col" title="the number of metrics scraped during the last scrape">Samples</th>
+                            <th scope="col" title="error from the last scrape (if any)">Last error</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    {% for _, ts := range jts.targetsStatus %}
+                    {% code
+                        endpoint := ts.sw.Config.ScrapeURL
+                        targetID := getTargetID(ts.sw)
+                        lastScrapeDuration := ts.getDurationFromLastScrape()
+                    %}
+                        <tr {% if !ts.up %}{%space%}class="alert alert-danger" role="alert" {% endif %}>
+                            <td class="endpoint">
+                                <a href="{%s endpoint %}" target="_blank">{%s endpoint %}</a> (
+                                <a href="target_response?id={%s targetID %}" target="_blank"
+                                  title="click to fetch target response on behalf of the scraper">response</a>
+                                )
+                            </td>
+                            <td>
+                                {% if ts.up %}
+                                    <span class="badge bg-success">UP</span>
+                                {% else %}
+                                    <span class="badge bg-danger">DOWN</span>
+                                {% endif %}
+                            </td>
+                            <td class="labels">
+                                <div title="click to show original labels"
+                                  onclick="document.getElementById('original-labels-{%s targetID %}').style.display='block'">
+                                    {%= formatLabel(promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels)) %}
+                                </div>
+                                <div style="display:none" id="original-labels-{%s targetID %}">
+                                    {%= formatLabel(ts.sw.Config.OriginalLabels) %}
+                                </div>
+                            </td>
+                            <td>{%d ts.scrapesTotal %}</td>
+                            <td>{%d ts.scrapesFailed %}</td>
+                            <td>
+                                {% if lastScrapeDuration < 365*24*time.Hour %}
+                                    {%d int(lastScrapeDuration.Milliseconds()) %}ms ago
+                                {% else %}
+                                    none
+                                {% endif %}
+                            <td>{%d int(ts.scrapeDuration) %}ms</td>
+                            <td>{%d ts.samplesScraped %}</td>
+                            <td>{% if ts.err != nil %}{%s ts.err.Error() %}{% endif %}</td>
+                        </tr>
+                    {% endfor %}
+                    </tbody>
+                </table>
+            </div>
         </div>
     </div>
-</body>
-</html>
 {% endfunc %}
 
-{% func queryArgs(m map[string]string) %}
+{% func discoveredTargets(tsr *targetsStatusResult) %}
+    {% code tljs := tsr.getTargetLabelsByJob() %}
+    <div class="row mt-4">
+        <div class="col-12">
+            {% for i, tlj := range tljs %}
+                {%= discoveredJobTargets(i, tlj) %}
+            {% endfor %}
+        </div>
+    </div>
+{% endfunc %}
+
+{% func discoveredJobTargets(num int, tlj *targetLabelsByJob) %}
+    <h4>
+        <span class="me-2">{%s tlj.jobName %}{% space %}({%d tlj.activeTargets %}/{%d tlj.activeTargets+tlj.droppedTargets %}{% space %}active)</span>
+        {%= showHideScrapeJobButtons(num) %}
+    </h4>
+    <div id="scrape-job-{%d num %}" class="scrape-job table-responsive">
+        <table class="table table-striped table-hover table-bordered table-sm">
+            <thead>
+                <tr>
+                    <th scope="col" style="width: 5%">Status</th>
+                    <th scope="col" style="width: 65%">Discovered Labels</th>
+                    <th scope="col" style="width: 30%">Target Labels</th>
+                </tr>
+            </thead>
+            <tbody>
+            {% for _, t := range tlj.targets %}
+                <tr
+                    {% if !t.up %}
+                        {% space %}role="alert"{% space %}
+                        {% if len(t.labels) > 0 %}
+                            class="alert alert-danger"
+                        {% else %}
+                            class="alert alert-warning"
+                        {% endif %}
+                    {% endif %}
+                >
+                    <td>
+                        {% if t.up %}
+                            <span class="badge bg-success">UP</span>
+                        {% elseif len(t.labels) > 0 %}
+                            <span class="badge bg-danger">DOWN</span>
+                        {% else %}
+                            <span class="badge bg-warning">DROPPED</span>
+                        {% endif %}
+                    </td>
+                    <td class="labels">
+                        {%= formatLabel(t.discoveredLabels) %}
+                    </td>
+                    <td class="labels">
+                        {%= formatLabel(promrelabel.FinalizeLabels(nil, t.labels)) %}
+                    </td>
+                </tr>
+            {% endfor %}
+            </tbody>
+        </table>
+    </div>
+{% endfunc %}
+
+{% func showHideScrapeJobButtons(num int) %}
+    <button type="button" class="btn btn-primary btn-sm me-1"
+      onclick="document.getElementById('scrape-job-{%d num %}').style.display='none'">
+        collapse
+    </button>
+    <button type="button" class="btn btn-secondary btn-sm me-1"
+      onclick="document.getElementById('scrape-job-{%d num %}').style.display='block'">
+        expand
+    </button>
+{% endfunc %}
+
+{% func queryArgs(filter *requestFilter, override map[string]string) %}
 {% code
+	showOnlyUnhealthy := "false"
+	if filter.showOnlyUnhealthy {
+		showOnlyUnhealthy = "true"
+	}
+	m := map[string]string{
+		"show_only_unhealthy": showOnlyUnhealthy,
+		"endpoint_search": filter.endpointSearch,
+		"label_search": filter.labelSearch,
+	}
+	for k, v := range override {
+		m[k] = v
+	}
 	qa := make(url.Values, len(m))
 	for k, v := range m {
 		qa[k] = []string{v}
diff --git a/lib/promscrape/targetstatus.qtpl.go b/lib/promscrape/targetstatus.qtpl.go
index 757de8756..c2a085da8 100644
--- a/lib/promscrape/targetstatus.qtpl.go
+++ b/lib/promscrape/targetstatus.qtpl.go
@@ -6,55 +6,52 @@ package promscrape
 
 //line lib/promscrape/targetstatus.qtpl:1
 import (
-	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
-	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
 	"net/url"
 	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
 )
 
-//line lib/promscrape/targetstatus.qtpl:10
+//line lib/promscrape/targetstatus.qtpl:11
 import (
 	qtio422016 "io"
 
 	qt422016 "github.com/valyala/quicktemplate"
 )
 
-//line lib/promscrape/targetstatus.qtpl:10
+//line lib/promscrape/targetstatus.qtpl:11
 var (
 	_ = qtio422016.Copy
 	_ = qt422016.AcquireByteBuffer
 )
 
-//line lib/promscrape/targetstatus.qtpl:10
-func StreamTargetsResponsePlain(qw422016 *qt422016.Writer, jts []jobTargetsStatuses, emptyJobs []string, showOriginLabels, showOnlyUnhealthy bool, err error) {
-//line lib/promscrape/targetstatus.qtpl:12
-	if err != nil {
+//line lib/promscrape/targetstatus.qtpl:11
+func StreamTargetsResponsePlain(qw422016 *qt422016.Writer, tsr *targetsStatusResult, filter *requestFilter) {
 //line lib/promscrape/targetstatus.qtpl:13
-		qw422016.N().S(err.Error())
+	if tsr.err != nil {
 //line lib/promscrape/targetstatus.qtpl:14
-		return
+		qw422016.N().S(tsr.err.Error())
 //line lib/promscrape/targetstatus.qtpl:15
+		return
+//line lib/promscrape/targetstatus.qtpl:16
 	}
-//line lib/promscrape/targetstatus.qtpl:17
-	for _, js := range jts {
 //line lib/promscrape/targetstatus.qtpl:18
-		if showOnlyUnhealthy && js.upCount == js.targetsTotal {
-//line lib/promscrape/targetstatus.qtpl:18
-			continue
-//line lib/promscrape/targetstatus.qtpl:18
-		}
+	for _, jts := range tsr.jobTargetsStatuses {
 //line lib/promscrape/targetstatus.qtpl:18
 		qw422016.N().S(`job=`)
 //line lib/promscrape/targetstatus.qtpl:19
-		qw422016.N().Q(js.job)
+		qw422016.N().S(jts.jobName)
+//line lib/promscrape/targetstatus.qtpl:19
+		qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:19
 		qw422016.N().S(`(`)
 //line lib/promscrape/targetstatus.qtpl:19
-		qw422016.N().D(js.upCount)
+		qw422016.N().D(jts.upCount)
 //line lib/promscrape/targetstatus.qtpl:19
 		qw422016.N().S(`/`)
 //line lib/promscrape/targetstatus.qtpl:19
-		qw422016.N().D(js.targetsTotal)
+		qw422016.N().D(jts.targetsTotal)
 //line lib/promscrape/targetstatus.qtpl:19
 		qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:19
@@ -63,571 +60,1044 @@ func StreamTargetsResponsePlain(qw422016 *qt422016.Writer, jts []jobTargetsStatu
 		qw422016.N().S(`
 `)
 //line lib/promscrape/targetstatus.qtpl:21
-		for _, ts := range js.targetsStatus {
-//line lib/promscrape/targetstatus.qtpl:22
-			if showOnlyUnhealthy && ts.up {
+		for _, ts := range jts.targetsStatus {
 //line lib/promscrape/targetstatus.qtpl:22
-				continue
-//line lib/promscrape/targetstatus.qtpl:22
-			}
-//line lib/promscrape/targetstatus.qtpl:23
 			qw422016.N().S("\t")
-//line lib/promscrape/targetstatus.qtpl:23
+//line lib/promscrape/targetstatus.qtpl:22
 			qw422016.N().S(`state=`)
-//line lib/promscrape/targetstatus.qtpl:24
+//line lib/promscrape/targetstatus.qtpl:23
 			if ts.up {
-//line lib/promscrape/targetstatus.qtpl:24
+//line lib/promscrape/targetstatus.qtpl:23
 				qw422016.N().S(`up`)
-//line lib/promscrape/targetstatus.qtpl:24
+//line lib/promscrape/targetstatus.qtpl:23
 			} else {
-//line lib/promscrape/targetstatus.qtpl:24
+//line lib/promscrape/targetstatus.qtpl:23
 				qw422016.N().S(`down`)
-//line lib/promscrape/targetstatus.qtpl:24
+//line lib/promscrape/targetstatus.qtpl:23
 			}
+//line lib/promscrape/targetstatus.qtpl:23
+			qw422016.N().S(`,`)
+//line lib/promscrape/targetstatus.qtpl:23
+			qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:23
+			qw422016.N().S(`endpoint=`)
+//line lib/promscrape/targetstatus.qtpl:24
+			qw422016.N().S(ts.sw.Config.ScrapeURL)
 //line lib/promscrape/targetstatus.qtpl:24
 			qw422016.N().S(`,`)
 //line lib/promscrape/targetstatus.qtpl:24
 			qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:24
-			qw422016.N().S(`endpoint=`)
+			qw422016.N().S(`labels=`)
 //line lib/promscrape/targetstatus.qtpl:25
-			qw422016.N().S(ts.sw.Config.ScrapeURL)
+			qw422016.N().S(promLabelsString(promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels)))
 //line lib/promscrape/targetstatus.qtpl:25
 			qw422016.N().S(`,`)
 //line lib/promscrape/targetstatus.qtpl:25
 			qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:25
-			qw422016.N().S(`labels=`)
-//line lib/promscrape/targetstatus.qtpl:26
-			qw422016.N().S(promLabelsString(promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels)))
 //line lib/promscrape/targetstatus.qtpl:26
-			qw422016.N().S(`,`)
+			if filter.showOriginalLabels {
 //line lib/promscrape/targetstatus.qtpl:26
-			qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:27
-			if showOriginLabels {
-//line lib/promscrape/targetstatus.qtpl:27
 				qw422016.N().S(`originalLabels=`)
-//line lib/promscrape/targetstatus.qtpl:27
+//line lib/promscrape/targetstatus.qtpl:26
 				qw422016.N().S(promLabelsString(ts.sw.Config.OriginalLabels))
-//line lib/promscrape/targetstatus.qtpl:27
+//line lib/promscrape/targetstatus.qtpl:26
 				qw422016.N().S(`,`)
-//line lib/promscrape/targetstatus.qtpl:27
+//line lib/promscrape/targetstatus.qtpl:26
 				qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:27
+//line lib/promscrape/targetstatus.qtpl:26
 			}
-//line lib/promscrape/targetstatus.qtpl:27
+//line lib/promscrape/targetstatus.qtpl:26
 			qw422016.N().S(`scrapes_total=`)
-//line lib/promscrape/targetstatus.qtpl:28
+//line lib/promscrape/targetstatus.qtpl:27
 			qw422016.N().D(ts.scrapesTotal)
+//line lib/promscrape/targetstatus.qtpl:27
+			qw422016.N().S(`,`)
+//line lib/promscrape/targetstatus.qtpl:27
+			qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:27
+			qw422016.N().S(`scrapes_failed=`)
+//line lib/promscrape/targetstatus.qtpl:28
+			qw422016.N().D(ts.scrapesFailed)
 //line lib/promscrape/targetstatus.qtpl:28
 			qw422016.N().S(`,`)
 //line lib/promscrape/targetstatus.qtpl:28
 			qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:28
-			qw422016.N().S(`scrapes_failed=`)
+			qw422016.N().S(`last_scrape=`)
 //line lib/promscrape/targetstatus.qtpl:29
-			qw422016.N().D(ts.scrapesFailed)
+			qw422016.N().D(int(ts.getDurationFromLastScrape().Milliseconds()))
 //line lib/promscrape/targetstatus.qtpl:29
-			qw422016.N().S(`,`)
+			qw422016.N().S(`ms ago,`)
 //line lib/promscrape/targetstatus.qtpl:29
 			qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:29
-			qw422016.N().S(`last_scrape=`)
+			qw422016.N().S(`scrape_duration=`)
 //line lib/promscrape/targetstatus.qtpl:30
-			qw422016.N().FPrec(ts.getDurationFromLastScrape().Seconds(), 3)
+			qw422016.N().D(int(ts.scrapeDuration))
 //line lib/promscrape/targetstatus.qtpl:30
-			qw422016.N().S(`s ago,`)
+			qw422016.N().S(`ms,`)
 //line lib/promscrape/targetstatus.qtpl:30
 			qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:30
-			qw422016.N().S(`scrape_duration=`)
+			qw422016.N().S(`samples_scraped=`)
 //line lib/promscrape/targetstatus.qtpl:31
-			qw422016.N().D(int(ts.scrapeDuration))
+			qw422016.N().D(ts.samplesScraped)
 //line lib/promscrape/targetstatus.qtpl:31
-			qw422016.N().S(`ms,`)
+			qw422016.N().S(`,`)
 //line lib/promscrape/targetstatus.qtpl:31
 			qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:31
-			qw422016.N().S(`samples_scraped=`)
-//line lib/promscrape/targetstatus.qtpl:32
-			qw422016.N().D(ts.samplesScraped)
-//line lib/promscrape/targetstatus.qtpl:32
-			qw422016.N().S(`,`)
-//line lib/promscrape/targetstatus.qtpl:32
-			qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:32
 			qw422016.N().S(`error=`)
-//line lib/promscrape/targetstatus.qtpl:33
+//line lib/promscrape/targetstatus.qtpl:32
 			if ts.err != nil {
-//line lib/promscrape/targetstatus.qtpl:33
+//line lib/promscrape/targetstatus.qtpl:32
 				qw422016.N().S(ts.err.Error())
-//line lib/promscrape/targetstatus.qtpl:33
+//line lib/promscrape/targetstatus.qtpl:32
 			}
-//line lib/promscrape/targetstatus.qtpl:34
+//line lib/promscrape/targetstatus.qtpl:33
 			qw422016.N().S(`
 `)
-//line lib/promscrape/targetstatus.qtpl:35
+//line lib/promscrape/targetstatus.qtpl:34
 		}
-//line lib/promscrape/targetstatus.qtpl:36
+//line lib/promscrape/targetstatus.qtpl:35
 	}
+//line lib/promscrape/targetstatus.qtpl:37
+	for _, jobName := range tsr.emptyJobs {
+//line lib/promscrape/targetstatus.qtpl:37
+		qw422016.N().S(`job=`)
 //line lib/promscrape/targetstatus.qtpl:38
-	for _, jobName := range emptyJobs {
+		qw422016.N().S(jobName)
+//line lib/promscrape/targetstatus.qtpl:38
+		qw422016.N().S(` `)
 //line lib/promscrape/targetstatus.qtpl:38
-		qw422016.N().S(`job=`)
-//line lib/promscrape/targetstatus.qtpl:39
-		qw422016.N().Q(jobName)
-//line lib/promscrape/targetstatus.qtpl:39
 		qw422016.N().S(`(0/0 up)`)
-//line lib/promscrape/targetstatus.qtpl:40
+//line lib/promscrape/targetstatus.qtpl:39
 		qw422016.N().S(`
 `)
-//line lib/promscrape/targetstatus.qtpl:41
+//line lib/promscrape/targetstatus.qtpl:40
 	}
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
 }
 
-//line lib/promscrape/targetstatus.qtpl:43
-func WriteTargetsResponsePlain(qq422016 qtio422016.Writer, jts []jobTargetsStatuses, emptyJobs []string, showOriginLabels, showOnlyUnhealthy bool, err error) {
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
+func WriteTargetsResponsePlain(qq422016 qtio422016.Writer, tsr *targetsStatusResult, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:42
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line lib/promscrape/targetstatus.qtpl:43
-	StreamTargetsResponsePlain(qw422016, jts, emptyJobs, showOriginLabels, showOnlyUnhealthy, err)
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
+	StreamTargetsResponsePlain(qw422016, tsr, filter)
+//line lib/promscrape/targetstatus.qtpl:42
 	qt422016.ReleaseWriter(qw422016)
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
 }
 
-//line lib/promscrape/targetstatus.qtpl:43
-func TargetsResponsePlain(jts []jobTargetsStatuses, emptyJobs []string, showOriginLabels, showOnlyUnhealthy bool, err error) string {
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
+func TargetsResponsePlain(tsr *targetsStatusResult, filter *requestFilter) string {
+//line lib/promscrape/targetstatus.qtpl:42
 	qb422016 := qt422016.AcquireByteBuffer()
-//line lib/promscrape/targetstatus.qtpl:43
-	WriteTargetsResponsePlain(qb422016, jts, emptyJobs, showOriginLabels, showOnlyUnhealthy, err)
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
+	WriteTargetsResponsePlain(qb422016, tsr, filter)
+//line lib/promscrape/targetstatus.qtpl:42
 	qs422016 := string(qb422016.B)
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
 	qt422016.ReleaseByteBuffer(qb422016)
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
 	return qs422016
-//line lib/promscrape/targetstatus.qtpl:43
+//line lib/promscrape/targetstatus.qtpl:42
 }
 
-//line lib/promscrape/targetstatus.qtpl:45
-func StreamTargetsResponseHTML(qw422016 *qt422016.Writer, jts []jobTargetsStatuses, emptyJobs []string, showOnlyUnhealthy bool, endpointSearch, labelSearch string, err error) {
-//line lib/promscrape/targetstatus.qtpl:45
-	qw422016.N().S(`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link href="static/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><title>Scrape targets</title><script>function collapse_all() {for (var i = 0; i <`)
+//line lib/promscrape/targetstatus.qtpl:44
+func StreamTargetsResponseHTML(qw422016 *qt422016.Writer, tsr *targetsStatusResult, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:44
+	qw422016.N().S(`<!DOCTYPE html><html lang="en"><head>`)
+//line lib/promscrape/targetstatus.qtpl:48
+	streamcommonHeader(qw422016)
+//line lib/promscrape/targetstatus.qtpl:48
+	qw422016.N().S(`<title>Active Targets</title></head><body>`)
+//line lib/promscrape/targetstatus.qtpl:52
+	streamnavbar(qw422016)
+//line lib/promscrape/targetstatus.qtpl:52
+	qw422016.N().S(`<div class="container-fluid">`)
+//line lib/promscrape/targetstatus.qtpl:54
+	if tsr.err != nil {
 //line lib/promscrape/targetstatus.qtpl:55
-	qw422016.N().D(len(jts))
-//line lib/promscrape/targetstatus.qtpl:55
-	qw422016.N().S(`; i++) {let el = document.getElementById("table-" + i);if (!el) {continue;}el.style.display = 'none';}}function expand_all() {for (var i = 0; i <`)
-//line lib/promscrape/targetstatus.qtpl:64
-	qw422016.N().D(len(jts))
-//line lib/promscrape/targetstatus.qtpl:64
-	qw422016.N().S(`; i++) {let el = document.getElementById("table-" + i);if (!el) {continue;}el.style.display = 'block';}}</script></head><body><div class="navbar navbar-dark bg-dark box-shadow"><div class="d-flex justify-content-between"><a href="#" class="navbar-brand d-flex align-items-center ms-3" title="The High Performance Open Source Time Series Database &amp; Monitoring Solution "><svg xmlns="http://www.w3.org/2000/svg" id="VM_logo" viewBox="0 0 464.61 533.89" width="20" height="20" class="me-1"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M459.86,467.77c9,7.67,24.12,13.49,39.3,13.69v0h1.68v0c15.18-.2,30.31-6,39.3-13.69,47.43-40.45,184.65-166.24,184.65-166.24,36.84-34.27-65.64-68.28-223.95-68.47h-1.68c-158.31.19-260.79,34.2-224,68.47C275.21,301.53,412.43,427.32,459.86,467.77Z" transform="translate(-267.7 -233.05)"/><path class="cls-1" d="M540.1,535.88c-9,7.67-24.12,13.5-39.3,13.7h-1.6c-15.18-.2-30.31-6-39.3-13.7-32.81-28-148.56-132.93-192.16-172.7v60.74c0,6.67,2.55,15.52,7.09,19.68,29.64,27.18,143.94,131.8,185.07,166.88,9,7.67,24.12,13.49,39.3,13.69v0h1.6v0c15.18-.2,30.31-6,39.3-13.69,41.13-35.08,155.43-139.7,185.07-166.88,4.54-4.16,7.09-13,7.09-19.68V363.18C688.66,403,572.91,507.9,540.1,535.88Z" transform="translate(-267.7 -233.05)"/><path class="cls-1" d="M540.1,678.64c-9,7.67-24.12,13.49-39.3,13.69v0h-1.6v0c-15.18-.2-30.31-6-39.3-13.69-32.81-28-148.56-132.94-192.16-172.7v60.73c0,6.67,2.55,15.53,7.09,19.69,29.64,27.17,143.94,131.8,185.07,166.87,9,7.67,24.12,13.5,39.3,13.7h1.6c15.18-.2,30.31-6,39.3-13.7,41.13-35.07,155.43-139.7,185.07-166.87,4.54-4.16,7.09-13,7.09-19.69V505.94C688.66,545.7,572.91,650.66,540.1,678.64Z" transform="translate(-267.7 -233.05)"/></svg><strong>VictoriaMetrics</strong></a></div></div><div class="container-fluid">`)
-//line lib/promscrape/targetstatus.qtpl:84
-	if err != nil {
-//line lib/promscrape/targetstatus.qtpl:85
-		streamerrorNotification(qw422016, err)
-//line lib/promscrape/targetstatus.qtpl:86
+		streamerrorNotification(qw422016, tsr.err)
+//line lib/promscrape/targetstatus.qtpl:56
 	}
-//line lib/promscrape/targetstatus.qtpl:86
-	qw422016.N().S(`<div class="row"><main class="col-12"><h1>Scrape targets</h1><hr /><div class="row g-3 align-items-center mb-3"><div class="col-auto"><button type="button" class="btn`)
-//line lib/promscrape/targetstatus.qtpl:93
+//line lib/promscrape/targetstatus.qtpl:56
+	qw422016.N().S(`<div class="row"><main class="col-12"><h1>Active Targets</h1><hr />`)
+//line lib/promscrape/targetstatus.qtpl:61
+	streamfiltersForm(qw422016, filter)
+//line lib/promscrape/targetstatus.qtpl:61
+	qw422016.N().S(`<hr />`)
+//line lib/promscrape/targetstatus.qtpl:63
+	streamtargetsTabs(qw422016, tsr, filter, "scrapeTargets")
+//line lib/promscrape/targetstatus.qtpl:63
+	qw422016.N().S(`</main></div></div></body></html>`)
+//line lib/promscrape/targetstatus.qtpl:69
+}
+
+//line lib/promscrape/targetstatus.qtpl:69
+func WriteTargetsResponseHTML(qq422016 qtio422016.Writer, tsr *targetsStatusResult, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:69
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:69
+	StreamTargetsResponseHTML(qw422016, tsr, filter)
+//line lib/promscrape/targetstatus.qtpl:69
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:69
+}
+
+//line lib/promscrape/targetstatus.qtpl:69
+func TargetsResponseHTML(tsr *targetsStatusResult, filter *requestFilter) string {
+//line lib/promscrape/targetstatus.qtpl:69
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:69
+	WriteTargetsResponseHTML(qb422016, tsr, filter)
+//line lib/promscrape/targetstatus.qtpl:69
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:69
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:69
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:69
+}
+
+//line lib/promscrape/targetstatus.qtpl:71
+func StreamServiceDiscoveryResponse(qw422016 *qt422016.Writer, tsr *targetsStatusResult, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:71
+	qw422016.N().S(`<!DOCTYPE html><html lang="en"><head>`)
+//line lib/promscrape/targetstatus.qtpl:75
+	streamcommonHeader(qw422016)
+//line lib/promscrape/targetstatus.qtpl:75
+	qw422016.N().S(`<title>Discovered Targets</title></head><body>`)
+//line lib/promscrape/targetstatus.qtpl:79
+	streamnavbar(qw422016)
+//line lib/promscrape/targetstatus.qtpl:79
+	qw422016.N().S(`<div class="container-fluid">`)
+//line lib/promscrape/targetstatus.qtpl:81
+	if tsr.err != nil {
+//line lib/promscrape/targetstatus.qtpl:82
+		streamerrorNotification(qw422016, tsr.err)
+//line lib/promscrape/targetstatus.qtpl:83
+	}
+//line lib/promscrape/targetstatus.qtpl:83
+	qw422016.N().S(`<div class="row"><main class="col-12"><h1>Discovered Targets</h1><hr />`)
+//line lib/promscrape/targetstatus.qtpl:88
+	streamfiltersForm(qw422016, filter)
+//line lib/promscrape/targetstatus.qtpl:88
+	qw422016.N().S(`<hr />`)
+//line lib/promscrape/targetstatus.qtpl:90
+	streamtargetsTabs(qw422016, tsr, filter, "discoveredTargets")
+//line lib/promscrape/targetstatus.qtpl:90
+	qw422016.N().S(`</main></div></div></body></html>`)
+//line lib/promscrape/targetstatus.qtpl:96
+}
+
+//line lib/promscrape/targetstatus.qtpl:96
+func WriteServiceDiscoveryResponse(qq422016 qtio422016.Writer, tsr *targetsStatusResult, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:96
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:96
+	StreamServiceDiscoveryResponse(qw422016, tsr, filter)
+//line lib/promscrape/targetstatus.qtpl:96
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:96
+}
+
+//line lib/promscrape/targetstatus.qtpl:96
+func ServiceDiscoveryResponse(tsr *targetsStatusResult, filter *requestFilter) string {
+//line lib/promscrape/targetstatus.qtpl:96
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:96
+	WriteServiceDiscoveryResponse(qb422016, tsr, filter)
+//line lib/promscrape/targetstatus.qtpl:96
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:96
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:96
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:96
+}
+
+//line lib/promscrape/targetstatus.qtpl:98
+func streamcommonHeader(qw422016 *qt422016.Writer) {
+//line lib/promscrape/targetstatus.qtpl:98
+	qw422016.N().S(`<meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><link href="static/css/bootstrap.min.css" rel="stylesheet" />`)
+//line lib/promscrape/targetstatus.qtpl:102
+}
+
+//line lib/promscrape/targetstatus.qtpl:102
+func writecommonHeader(qq422016 qtio422016.Writer) {
+//line lib/promscrape/targetstatus.qtpl:102
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:102
+	streamcommonHeader(qw422016)
+//line lib/promscrape/targetstatus.qtpl:102
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:102
+}
+
+//line lib/promscrape/targetstatus.qtpl:102
+func commonHeader() string {
+//line lib/promscrape/targetstatus.qtpl:102
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:102
+	writecommonHeader(qb422016)
+//line lib/promscrape/targetstatus.qtpl:102
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:102
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:102
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:102
+}
+
+//line lib/promscrape/targetstatus.qtpl:104
+func streamnavbar(qw422016 *qt422016.Writer) {
+//line lib/promscrape/targetstatus.qtpl:104
+	qw422016.N().S(`<div class="navbar navbar-dark bg-dark box-shadow"><div class="d-flex justify-content-between"><a href="#" class="navbar-brand d-flex align-items-center ms-3" title="The High Performance Open Source Time Series Database &amp; Monitoring Solution "><svg xmlns="http://www.w3.org/2000/svg" id="VM_logo" viewBox="0 0 464.61 533.89" width="20" height="20" class="me-1"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M459.86,467.77c9,7.67,24.12,13.49,39.3,13.69v0h1.68v0c15.18-.2,30.31-6,39.3-13.69,47.43-40.45,184.65-166.24,184.65-166.24,36.84-34.27-65.64-68.28-223.95-68.47h-1.68c-158.31.19-260.79,34.2-224,68.47C275.21,301.53,412.43,427.32,459.86,467.77Z" transform="translate(-267.7 -233.05)"/><path class="cls-1" d="M540.1,535.88c-9,7.67-24.12,13.5-39.3,13.7h-1.6c-15.18-.2-30.31-6-39.3-13.7-32.81-28-148.56-132.93-192.16-172.7v60.74c0,6.67,2.55,15.52,7.09,19.68,29.64,27.18,143.94,131.8,185.07,166.88,9,7.67,24.12,13.49,39.3,13.69v0h1.6v0c15.18-.2,30.31-6,39.3-13.69,41.13-35.08,155.43-139.7,185.07-166.88,4.54-4.16,7.09-13,7.09-19.68V363.18C688.66,403,572.91,507.9,540.1,535.88Z" transform="translate(-267.7 -233.05)"/><path class="cls-1" d="M540.1,678.64c-9,7.67-24.12,13.49-39.3,13.69v0h-1.6v0c-15.18-.2-30.31-6-39.3-13.69-32.81-28-148.56-132.94-192.16-172.7v60.73c0,6.67,2.55,15.53,7.09,19.69,29.64,27.17,143.94,131.8,185.07,166.87,9,7.67,24.12,13.5,39.3,13.7h1.6c15.18-.2,30.31-6,39.3-13.7,41.13-35.07,155.43-139.7,185.07-166.87,4.54-4.16,7.09-13,7.09-19.69V505.94C688.66,545.7,572.91,650.66,540.1,678.64Z" transform="translate(-267.7 -233.05)"/></svg><strong>VictoriaMetrics</strong></a></div></div>`)
+//line lib/promscrape/targetstatus.qtpl:113
+}
+
+//line lib/promscrape/targetstatus.qtpl:113
+func writenavbar(qq422016 qtio422016.Writer) {
+//line lib/promscrape/targetstatus.qtpl:113
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:113
+	streamnavbar(qw422016)
+//line lib/promscrape/targetstatus.qtpl:113
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:113
+}
+
+//line lib/promscrape/targetstatus.qtpl:113
+func navbar() string {
+//line lib/promscrape/targetstatus.qtpl:113
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:113
+	writenavbar(qb422016)
+//line lib/promscrape/targetstatus.qtpl:113
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:113
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:113
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:113
+}
+
+//line lib/promscrape/targetstatus.qtpl:115
+func streamfiltersForm(qw422016 *qt422016.Writer, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:115
+	qw422016.N().S(`<div class="row g-3 align-items-center mb-3"><div class="col-auto"><button id="all-btn" type="button" class="btn`)
+//line lib/promscrape/targetstatus.qtpl:118
 	qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:93
-	if !showOnlyUnhealthy {
-//line lib/promscrape/targetstatus.qtpl:93
+//line lib/promscrape/targetstatus.qtpl:118
+	if !filter.showOnlyUnhealthy {
+//line lib/promscrape/targetstatus.qtpl:118
 		qw422016.N().S(`btn-secondary`)
-//line lib/promscrape/targetstatus.qtpl:93
+//line lib/promscrape/targetstatus.qtpl:118
 	} else {
-//line lib/promscrape/targetstatus.qtpl:93
+//line lib/promscrape/targetstatus.qtpl:118
 		qw422016.N().S(`btn-success`)
-//line lib/promscrape/targetstatus.qtpl:93
+//line lib/promscrape/targetstatus.qtpl:118
 	}
-//line lib/promscrape/targetstatus.qtpl:93
-	qw422016.N().S(`" onclick="location.href='?`)
-//line lib/promscrape/targetstatus.qtpl:93
-	streamqueryArgs(qw422016, map[string]string{
-		"show_only_unhealthy": "false",
-		"endpoint_search":     endpointSearch,
-		"label_search":        labelSearch,
-	})
-//line lib/promscrape/targetstatus.qtpl:97
-	qw422016.N().S(`'">All</button></div><div class="col-auto"><button type="button" class="btn`)
-//line lib/promscrape/targetstatus.qtpl:102
+//line lib/promscrape/targetstatus.qtpl:118
+	qw422016.N().S(`"onclick="location.href='?`)
+//line lib/promscrape/targetstatus.qtpl:119
+	streamqueryArgs(qw422016, filter, map[string]string{"show_only_unhealthy": "false"})
+//line lib/promscrape/targetstatus.qtpl:119
+	qw422016.N().S(`'">All</button></div><div class="col-auto"><button id="unhealthy-btn" type="button" class="btn`)
+//line lib/promscrape/targetstatus.qtpl:124
 	qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:102
-	if showOnlyUnhealthy {
-//line lib/promscrape/targetstatus.qtpl:102
+//line lib/promscrape/targetstatus.qtpl:124
+	if filter.showOnlyUnhealthy {
+//line lib/promscrape/targetstatus.qtpl:124
 		qw422016.N().S(`btn-secondary`)
-//line lib/promscrape/targetstatus.qtpl:102
+//line lib/promscrape/targetstatus.qtpl:124
 	} else {
-//line lib/promscrape/targetstatus.qtpl:102
+//line lib/promscrape/targetstatus.qtpl:124
 		qw422016.N().S(`btn-danger`)
-//line lib/promscrape/targetstatus.qtpl:102
+//line lib/promscrape/targetstatus.qtpl:124
 	}
-//line lib/promscrape/targetstatus.qtpl:102
-	qw422016.N().S(`" onclick="location.href='?`)
-//line lib/promscrape/targetstatus.qtpl:102
-	streamqueryArgs(qw422016, map[string]string{
-		"show_only_unhealthy": "true",
-		"endpoint_search":     endpointSearch,
-		"label_search":        labelSearch,
-	})
-//line lib/promscrape/targetstatus.qtpl:106
-	qw422016.N().S(`'">Unhealthy</button></div><div class="col-auto"><button type="button" class="btn btn-primary" onclick="collapse_all()">Collapse all</button></div><div class="col-auto"><button type="button" class="btn btn-secondary" onclick="expand_all()">Expand all</button></div><div class="col-auto">`)
-//line lib/promscrape/targetstatus.qtpl:121
-	if endpointSearch == "" && labelSearch == "" {
-//line lib/promscrape/targetstatus.qtpl:121
-		qw422016.N().S(`<button type="button" class="btn btn-success" onclick="document.getElementById('filters').style.display='block'">Filter targets</button>`)
+//line lib/promscrape/targetstatus.qtpl:124
+	qw422016.N().S(`"onclick="location.href='?`)
 //line lib/promscrape/targetstatus.qtpl:125
-	} else {
+	streamqueryArgs(qw422016, filter, map[string]string{"show_only_unhealthy": "true"})
 //line lib/promscrape/targetstatus.qtpl:125
-		qw422016.N().S(`<button type="button" class="btn btn-danger" onclick="location.href='?'">Clear target filters</button>`)
-//line lib/promscrape/targetstatus.qtpl:129
-	}
-//line lib/promscrape/targetstatus.qtpl:129
-	qw422016.N().S(`</div></div><div id="filters"`)
-//line lib/promscrape/targetstatus.qtpl:132
-	if endpointSearch == "" && labelSearch == "" {
-//line lib/promscrape/targetstatus.qtpl:132
+	qw422016.N().S(`'">Unhealthy</button></div><div class="col-auto"><button type="button" class="btn btn-primary" onclick="document.querySelectorAll('.scrape-job').forEach((el) => { el.style.display = 'none'; })">Collapse all</button></div><div class="col-auto"><button type="button" class="btn btn-secondary" onclick="document.querySelectorAll('.scrape-job').forEach((el) => { el.style.display = 'block'; })">Expand all</button></div><div class="col-auto"><button type="button" class="btn btn-success" onclick="document.getElementById('filters').style.display='block'">Filter targets</button></div></div><div id="filters"`)
+//line lib/promscrape/targetstatus.qtpl:145
+	if filter.endpointSearch == "" && filter.labelSearch == "" {
+//line lib/promscrape/targetstatus.qtpl:145
 		qw422016.N().S(`style="display:none"`)
-//line lib/promscrape/targetstatus.qtpl:132
+//line lib/promscrape/targetstatus.qtpl:145
 	}
-//line lib/promscrape/targetstatus.qtpl:132
+//line lib/promscrape/targetstatus.qtpl:145
 	qw422016.N().S(`><form class="form-horizontal"><div class="form-group mb-3"><label for="endpoint_search" class="col-sm-10 control-label">Endpoint filter (<a target="_blank" href="https://github.com/google/re2/wiki/Syntax">Regexp</a> is accepted)</label><div class="col-sm-10"><input type="text" id="endpoint_search" name="endpoint_search"placeholder="For example, 127.0.0.1" class="form-control" value="`)
-//line lib/promscrape/targetstatus.qtpl:138
-	qw422016.E().S(endpointSearch)
-//line lib/promscrape/targetstatus.qtpl:138
+//line lib/promscrape/targetstatus.qtpl:151
+	qw422016.E().S(filter.endpointSearch)
+//line lib/promscrape/targetstatus.qtpl:151
 	qw422016.N().S(`"/></div></div><div class="form-group mb-3"><label for="label_search" class="col-sm-10 control-label">Labels filter (<a target="_blank" href="https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors">Arbitrary time series selectors</a> are accepted)</label><div class="col-sm-10"><input type="text" id="label_search" name="label_search"placeholder="For example, {instance=~'.+:9100'}" class="form-control" value="`)
-//line lib/promscrape/targetstatus.qtpl:145
-	qw422016.E().S(labelSearch)
-//line lib/promscrape/targetstatus.qtpl:145
+//line lib/promscrape/targetstatus.qtpl:158
+	qw422016.E().S(filter.labelSearch)
+//line lib/promscrape/targetstatus.qtpl:158
 	qw422016.N().S(`"/></div></div><input type="hidden" name="show_only_unhealthy" value="`)
-//line lib/promscrape/targetstatus.qtpl:148
-	qw422016.E().V(showOnlyUnhealthy)
-//line lib/promscrape/targetstatus.qtpl:148
-	qw422016.N().S(`"/><button type="submit" class="btn btn-success mb-3">Submit</button></form></div><hr /><div class="row"><div class="col-12">`)
-//line lib/promscrape/targetstatus.qtpl:155
-	for i, js := range jts {
-//line lib/promscrape/targetstatus.qtpl:156
-		if showOnlyUnhealthy && js.upCount == js.targetsTotal {
-//line lib/promscrape/targetstatus.qtpl:156
-			continue
-//line lib/promscrape/targetstatus.qtpl:156
-		}
-//line lib/promscrape/targetstatus.qtpl:156
-		qw422016.N().S(`<div class="row mb-4"><div class="col-12"><h4><span class="me-2">`)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.E().S(js.job)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().S(`(`)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().D(js.upCount)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().S(`/`)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().D(js.targetsTotal)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:160
-		qw422016.N().S(`up)</span><button type="button" class="btn btn-primary btn-sm me-1"onclick="document.getElementById('table-`)
+//line lib/promscrape/targetstatus.qtpl:161
+	qw422016.E().V(filter.showOnlyUnhealthy)
+//line lib/promscrape/targetstatus.qtpl:161
+	qw422016.N().S(`"/><input type="hidden" name="show_original_labels" value="`)
 //line lib/promscrape/targetstatus.qtpl:162
-		qw422016.N().D(i)
+	qw422016.E().V(filter.showOriginalLabels)
 //line lib/promscrape/targetstatus.qtpl:162
-		qw422016.N().S(`').style.display='none'">collapse</button><button type="button" class="btn btn-secondary btn-sm me-1"onclick="document.getElementById('table-`)
-//line lib/promscrape/targetstatus.qtpl:165
-		qw422016.N().D(i)
-//line lib/promscrape/targetstatus.qtpl:165
-		qw422016.N().S(`').style.display='block'">expand</button></h4><div id="table-`)
-//line lib/promscrape/targetstatus.qtpl:168
-		qw422016.N().D(i)
-//line lib/promscrape/targetstatus.qtpl:168
-		qw422016.N().S(`" class="table-responsive"><table class="table table-striped table-hover table-bordered table-sm"><thead><tr><th scope="col">Endpoint</th><th scope="col">State</th><th scope="col" title="scrape target labels">Labels</th><th scope="col" title="total scrapes">Scrapes</th><th scope="col" title="total scrape errors">Errors</th><th scope="col" title="the time of the last scrape">Last Scrape</th><th scope="col" title="the duration of the last scrape">Duration</th><th scope="col" title="the number of metrics scraped during the last scrape">Samples</th><th scope="col" title="error from the last scrape (if any)">Last error</th></tr></thead><tbody class="list-`)
-//line lib/promscrape/targetstatus.qtpl:183
-		qw422016.N().D(i)
-//line lib/promscrape/targetstatus.qtpl:183
-		qw422016.N().S(`">`)
-//line lib/promscrape/targetstatus.qtpl:184
-		for _, ts := range js.targetsStatus {
-//line lib/promscrape/targetstatus.qtpl:186
-			endpoint := ts.sw.Config.ScrapeURL
-			targetID := getTargetID(ts.sw)
-			lastScrapeTime := ts.getDurationFromLastScrape()
+	qw422016.N().S(`"/><button type="submit" class="btn btn-success mb-3">Submit</button><button type="button" class="btn btn-danger mb-3" onclick="location.href='?'">Clear target filters</button></form></div>`)
+//line lib/promscrape/targetstatus.qtpl:167
+}
 
+//line lib/promscrape/targetstatus.qtpl:167
+func writefiltersForm(qq422016 qtio422016.Writer, filter *requestFilter) {
+//line lib/promscrape/targetstatus.qtpl:167
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:167
+	streamfiltersForm(qw422016, filter)
+//line lib/promscrape/targetstatus.qtpl:167
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:167
+}
+
+//line lib/promscrape/targetstatus.qtpl:167
+func filtersForm(filter *requestFilter) string {
+//line lib/promscrape/targetstatus.qtpl:167
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:167
+	writefiltersForm(qb422016, filter)
+//line lib/promscrape/targetstatus.qtpl:167
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:167
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:167
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:167
+}
+
+//line lib/promscrape/targetstatus.qtpl:169
+func streamtargetsTabs(qw422016 *qt422016.Writer, tsr *targetsStatusResult, filter *requestFilter, activeTab string) {
+//line lib/promscrape/targetstatus.qtpl:169
+	qw422016.N().S(`<ul class="nav nav-tabs" id="myTab" role="tablist"><li class="nav-item" role="presentation"><button class="nav-link`)
+//line lib/promscrape/targetstatus.qtpl:172
+	if activeTab == "scrapeTargets" {
+//line lib/promscrape/targetstatus.qtpl:172
+		qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:172
+		qw422016.N().S(`active`)
+//line lib/promscrape/targetstatus.qtpl:172
+	}
+//line lib/promscrape/targetstatus.qtpl:172
+	qw422016.N().S(`" type="button" role="tab"onclick="location.href='targets?`)
+//line lib/promscrape/targetstatus.qtpl:173
+	streamqueryArgs(qw422016, filter, nil)
+//line lib/promscrape/targetstatus.qtpl:173
+	qw422016.N().S(`'">Active targets</button></li><li class="nav-item" role="presentation"><button class="nav-link`)
+//line lib/promscrape/targetstatus.qtpl:178
+	if activeTab == "discoveredTargets" {
+//line lib/promscrape/targetstatus.qtpl:178
+		qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:178
+		qw422016.N().S(`active`)
+//line lib/promscrape/targetstatus.qtpl:178
+	}
+//line lib/promscrape/targetstatus.qtpl:178
+	qw422016.N().S(`" type="button" role="tab"onclick="location.href='service-discovery?`)
+//line lib/promscrape/targetstatus.qtpl:179
+	streamqueryArgs(qw422016, filter, nil)
+//line lib/promscrape/targetstatus.qtpl:179
+	qw422016.N().S(`'">Discovered targets</button></li></ul><div class="tab-content"><div class="tab-pane active" role="tabpanel">`)
+//line lib/promscrape/targetstatus.qtpl:186
+	switch activeTab {
+//line lib/promscrape/targetstatus.qtpl:187
+	case "scrapeTargets":
+//line lib/promscrape/targetstatus.qtpl:188
+		streamscrapeTargets(qw422016, tsr)
+//line lib/promscrape/targetstatus.qtpl:189
+	case "discoveredTargets":
 //line lib/promscrape/targetstatus.qtpl:190
-			if showOnlyUnhealthy && ts.up {
-//line lib/promscrape/targetstatus.qtpl:190
-				continue
-//line lib/promscrape/targetstatus.qtpl:190
-			}
-//line lib/promscrape/targetstatus.qtpl:190
-			qw422016.N().S(`<tr`)
-//line lib/promscrape/targetstatus.qtpl:191
-			if !ts.up {
+		streamdiscoveredTargets(qw422016, tsr)
 //line lib/promscrape/targetstatus.qtpl:191
-				qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:191
-				qw422016.N().S(`class="alert alert-danger" role="alert"`)
-//line lib/promscrape/targetstatus.qtpl:191
-			}
+	}
 //line lib/promscrape/targetstatus.qtpl:191
-			qw422016.N().S(`><td class="endpoint"><a href="`)
-//line lib/promscrape/targetstatus.qtpl:192
-			qw422016.E().S(endpoint)
-//line lib/promscrape/targetstatus.qtpl:192
-			qw422016.N().S(`" target="_blank">`)
-//line lib/promscrape/targetstatus.qtpl:192
-			qw422016.E().S(endpoint)
-//line lib/promscrape/targetstatus.qtpl:192
-			qw422016.N().S(`</a> (<a href="target_response?id=`)
-//line lib/promscrape/targetstatus.qtpl:193
-			qw422016.E().S(targetID)
-//line lib/promscrape/targetstatus.qtpl:193
-			qw422016.N().S(`" target="_blank"title="click to fetch target response on behalf of the scraper">response</a>)</td><td>`)
-//line lib/promscrape/targetstatus.qtpl:197
-			if ts.up {
-//line lib/promscrape/targetstatus.qtpl:197
-				qw422016.N().S(`UP`)
-//line lib/promscrape/targetstatus.qtpl:197
-			} else {
-//line lib/promscrape/targetstatus.qtpl:197
-				qw422016.N().S(`DOWN`)
-//line lib/promscrape/targetstatus.qtpl:197
-			}
-//line lib/promscrape/targetstatus.qtpl:197
-			qw422016.N().S(`</td><td class="labels"><div title="click to show original labels"onclick="document.getElementById('original_labels_`)
-//line lib/promscrape/targetstatus.qtpl:200
-			qw422016.E().S(targetID)
+	qw422016.N().S(`</div></div>`)
+//line lib/promscrape/targetstatus.qtpl:194
+}
+
+//line lib/promscrape/targetstatus.qtpl:194
+func writetargetsTabs(qq422016 qtio422016.Writer, tsr *targetsStatusResult, filter *requestFilter, activeTab string) {
+//line lib/promscrape/targetstatus.qtpl:194
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:194
+	streamtargetsTabs(qw422016, tsr, filter, activeTab)
+//line lib/promscrape/targetstatus.qtpl:194
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:194
+}
+
+//line lib/promscrape/targetstatus.qtpl:194
+func targetsTabs(tsr *targetsStatusResult, filter *requestFilter, activeTab string) string {
+//line lib/promscrape/targetstatus.qtpl:194
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:194
+	writetargetsTabs(qb422016, tsr, filter, activeTab)
+//line lib/promscrape/targetstatus.qtpl:194
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:194
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:194
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:194
+}
+
+//line lib/promscrape/targetstatus.qtpl:196
+func streamscrapeTargets(qw422016 *qt422016.Writer, tsr *targetsStatusResult) {
+//line lib/promscrape/targetstatus.qtpl:196
+	qw422016.N().S(`<div class="row mt-4"><div class="col-12">`)
+//line lib/promscrape/targetstatus.qtpl:199
+	for i, jts := range tsr.jobTargetsStatuses {
 //line lib/promscrape/targetstatus.qtpl:200
-			qw422016.N().S(`').style.display='block'">`)
-//line lib/promscrape/targetstatus.qtpl:201
-			streamformatLabel(qw422016, promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels))
+		streamscrapeJobTargets(qw422016, i, jts)
 //line lib/promscrape/targetstatus.qtpl:201
-			qw422016.N().S(`</div><div style="display:none" id="original_labels_`)
-//line lib/promscrape/targetstatus.qtpl:203
-			qw422016.E().S(targetID)
-//line lib/promscrape/targetstatus.qtpl:203
-			qw422016.N().S(`">`)
-//line lib/promscrape/targetstatus.qtpl:204
-			streamformatLabel(qw422016, ts.sw.Config.OriginalLabels)
+	}
+//line lib/promscrape/targetstatus.qtpl:202
+	for i, jobName := range tsr.emptyJobs {
 //line lib/promscrape/targetstatus.qtpl:204
-			qw422016.N().S(`</div></td><td>`)
-//line lib/promscrape/targetstatus.qtpl:207
-			qw422016.N().D(ts.scrapesTotal)
-//line lib/promscrape/targetstatus.qtpl:207
-			qw422016.N().S(`</td><td>`)
-//line lib/promscrape/targetstatus.qtpl:208
-			qw422016.N().D(ts.scrapesFailed)
-//line lib/promscrape/targetstatus.qtpl:208
-			qw422016.N().S(`</td><td>`)
+		num := i + len(tsr.jobTargetsStatuses)
+		jts := &jobTargetsStatuses{
+			jobName: jobName,
+		}
+
+//line lib/promscrape/targetstatus.qtpl:209
+		streamscrapeJobTargets(qw422016, num, jts)
 //line lib/promscrape/targetstatus.qtpl:210
-			if lastScrapeTime < 365*24*time.Hour {
-//line lib/promscrape/targetstatus.qtpl:211
-				qw422016.N().FPrec(lastScrapeTime.Seconds(), 3)
-//line lib/promscrape/targetstatus.qtpl:211
-				qw422016.N().S(`s ago`)
-//line lib/promscrape/targetstatus.qtpl:212
-			} else {
-//line lib/promscrape/targetstatus.qtpl:212
-				qw422016.N().S(`none`)
-//line lib/promscrape/targetstatus.qtpl:214
-			}
-//line lib/promscrape/targetstatus.qtpl:214
-			qw422016.N().S(`<td>`)
+	}
+//line lib/promscrape/targetstatus.qtpl:210
+	qw422016.N().S(`</div></div>`)
+//line lib/promscrape/targetstatus.qtpl:213
+}
+
+//line lib/promscrape/targetstatus.qtpl:213
+func writescrapeTargets(qq422016 qtio422016.Writer, tsr *targetsStatusResult) {
+//line lib/promscrape/targetstatus.qtpl:213
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:213
+	streamscrapeTargets(qw422016, tsr)
+//line lib/promscrape/targetstatus.qtpl:213
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:213
+}
+
+//line lib/promscrape/targetstatus.qtpl:213
+func scrapeTargets(tsr *targetsStatusResult) string {
+//line lib/promscrape/targetstatus.qtpl:213
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:213
+	writescrapeTargets(qb422016, tsr)
+//line lib/promscrape/targetstatus.qtpl:213
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:213
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:213
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:213
+}
+
 //line lib/promscrape/targetstatus.qtpl:215
-			qw422016.N().D(int(ts.scrapeDuration))
+func streamscrapeJobTargets(qw422016 *qt422016.Writer, num int, jts *jobTargetsStatuses) {
 //line lib/promscrape/targetstatus.qtpl:215
-			qw422016.N().S(`ms</td><td>`)
-//line lib/promscrape/targetstatus.qtpl:216
-			qw422016.N().D(ts.samplesScraped)
-//line lib/promscrape/targetstatus.qtpl:216
-			qw422016.N().S(`</td><td>`)
-//line lib/promscrape/targetstatus.qtpl:217
-			if ts.err != nil {
-//line lib/promscrape/targetstatus.qtpl:217
-				qw422016.E().S(ts.err.Error())
-//line lib/promscrape/targetstatus.qtpl:217
-			}
-//line lib/promscrape/targetstatus.qtpl:217
-			qw422016.N().S(`</td></tr>`)
+	qw422016.N().S(`<div class="row mb-4"><div class="col-12"><h4><span class="me-2">`)
 //line lib/promscrape/targetstatus.qtpl:219
-		}
+	qw422016.E().S(jts.jobName)
+//line lib/promscrape/targetstatus.qtpl:219
+	qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:219
+	qw422016.N().S(`(`)
 //line lib/promscrape/targetstatus.qtpl:219
-		qw422016.N().S(`</tbody></table></div></div></div>`)
-//line lib/promscrape/targetstatus.qtpl:225
+	qw422016.N().D(jts.upCount)
+//line lib/promscrape/targetstatus.qtpl:219
+	qw422016.N().S(`/`)
+//line lib/promscrape/targetstatus.qtpl:219
+	qw422016.N().D(jts.targetsTotal)
+//line lib/promscrape/targetstatus.qtpl:219
+	qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:219
+	qw422016.N().S(`up)</span>`)
+//line lib/promscrape/targetstatus.qtpl:220
+	streamshowHideScrapeJobButtons(qw422016, num)
+//line lib/promscrape/targetstatus.qtpl:220
+	qw422016.N().S(`</h4><div id="scrape-job-`)
+//line lib/promscrape/targetstatus.qtpl:222
+	qw422016.N().D(num)
+//line lib/promscrape/targetstatus.qtpl:222
+	qw422016.N().S(`" class="scrape-job table-responsive"><table class="table table-striped table-hover table-bordered table-sm"><thead><tr><th scope="col">Endpoint</th><th scope="col">State</th><th scope="col" title="target labels">Labels</th><th scope="col" title="total scrapes">Scrapes</th><th scope="col" title="total scrape errors">Errors</th><th scope="col" title="the time of the last scrape">Last Scrape</th><th scope="col" title="the duration of the last scrape">Duration</th><th scope="col" title="the number of metrics scraped during the last scrape">Samples</th><th scope="col" title="error from the last scrape (if any)">Last error</th></tr></thead><tbody>`)
+//line lib/promscrape/targetstatus.qtpl:238
+	for _, ts := range jts.targetsStatus {
+//line lib/promscrape/targetstatus.qtpl:240
+		endpoint := ts.sw.Config.ScrapeURL
+		targetID := getTargetID(ts.sw)
+		lastScrapeDuration := ts.getDurationFromLastScrape()
+
+//line lib/promscrape/targetstatus.qtpl:243
+		qw422016.N().S(`<tr`)
+//line lib/promscrape/targetstatus.qtpl:244
+		if !ts.up {
+//line lib/promscrape/targetstatus.qtpl:244
+			qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:244
+			qw422016.N().S(`class="alert alert-danger" role="alert"`)
+//line lib/promscrape/targetstatus.qtpl:244
+		}
+//line lib/promscrape/targetstatus.qtpl:244
+		qw422016.N().S(`><td class="endpoint"><a href="`)
+//line lib/promscrape/targetstatus.qtpl:246
+		qw422016.E().S(endpoint)
+//line lib/promscrape/targetstatus.qtpl:246
+		qw422016.N().S(`" target="_blank">`)
+//line lib/promscrape/targetstatus.qtpl:246
+		qw422016.E().S(endpoint)
+//line lib/promscrape/targetstatus.qtpl:246
+		qw422016.N().S(`</a> (<a href="target_response?id=`)
+//line lib/promscrape/targetstatus.qtpl:247
+		qw422016.E().S(targetID)
+//line lib/promscrape/targetstatus.qtpl:247
+		qw422016.N().S(`" target="_blank"title="click to fetch target response on behalf of the scraper">response</a>)</td><td>`)
+//line lib/promscrape/targetstatus.qtpl:252
+		if ts.up {
+//line lib/promscrape/targetstatus.qtpl:252
+			qw422016.N().S(`<span class="badge bg-success">UP</span>`)
+//line lib/promscrape/targetstatus.qtpl:254
+		} else {
+//line lib/promscrape/targetstatus.qtpl:254
+			qw422016.N().S(`<span class="badge bg-danger">DOWN</span>`)
+//line lib/promscrape/targetstatus.qtpl:256
+		}
+//line lib/promscrape/targetstatus.qtpl:256
+		qw422016.N().S(`</td><td class="labels"><div title="click to show original labels"onclick="document.getElementById('original-labels-`)
+//line lib/promscrape/targetstatus.qtpl:260
+		qw422016.E().S(targetID)
+//line lib/promscrape/targetstatus.qtpl:260
+		qw422016.N().S(`').style.display='block'">`)
+//line lib/promscrape/targetstatus.qtpl:261
+		streamformatLabel(qw422016, promrelabel.FinalizeLabels(nil, ts.sw.Config.Labels))
+//line lib/promscrape/targetstatus.qtpl:261
+		qw422016.N().S(`</div><div style="display:none" id="original-labels-`)
+//line lib/promscrape/targetstatus.qtpl:263
+		qw422016.E().S(targetID)
+//line lib/promscrape/targetstatus.qtpl:263
+		qw422016.N().S(`">`)
+//line lib/promscrape/targetstatus.qtpl:264
+		streamformatLabel(qw422016, ts.sw.Config.OriginalLabels)
+//line lib/promscrape/targetstatus.qtpl:264
+		qw422016.N().S(`</div></td><td>`)
+//line lib/promscrape/targetstatus.qtpl:267
+		qw422016.N().D(ts.scrapesTotal)
+//line lib/promscrape/targetstatus.qtpl:267
+		qw422016.N().S(`</td><td>`)
+//line lib/promscrape/targetstatus.qtpl:268
+		qw422016.N().D(ts.scrapesFailed)
+//line lib/promscrape/targetstatus.qtpl:268
+		qw422016.N().S(`</td><td>`)
+//line lib/promscrape/targetstatus.qtpl:270
+		if lastScrapeDuration < 365*24*time.Hour {
+//line lib/promscrape/targetstatus.qtpl:271
+			qw422016.N().D(int(lastScrapeDuration.Milliseconds()))
+//line lib/promscrape/targetstatus.qtpl:271
+			qw422016.N().S(`ms ago`)
+//line lib/promscrape/targetstatus.qtpl:272
+		} else {
+//line lib/promscrape/targetstatus.qtpl:272
+			qw422016.N().S(`none`)
+//line lib/promscrape/targetstatus.qtpl:274
+		}
+//line lib/promscrape/targetstatus.qtpl:274
+		qw422016.N().S(`<td>`)
+//line lib/promscrape/targetstatus.qtpl:275
+		qw422016.N().D(int(ts.scrapeDuration))
+//line lib/promscrape/targetstatus.qtpl:275
+		qw422016.N().S(`ms</td><td>`)
+//line lib/promscrape/targetstatus.qtpl:276
+		qw422016.N().D(ts.samplesScraped)
+//line lib/promscrape/targetstatus.qtpl:276
+		qw422016.N().S(`</td><td>`)
+//line lib/promscrape/targetstatus.qtpl:277
+		if ts.err != nil {
+//line lib/promscrape/targetstatus.qtpl:277
+			qw422016.E().S(ts.err.Error())
+//line lib/promscrape/targetstatus.qtpl:277
+		}
+//line lib/promscrape/targetstatus.qtpl:277
+		qw422016.N().S(`</td></tr>`)
+//line lib/promscrape/targetstatus.qtpl:279
+	}
+//line lib/promscrape/targetstatus.qtpl:279
+	qw422016.N().S(`</tbody></table></div></div></div>`)
+//line lib/promscrape/targetstatus.qtpl:285
+}
+
+//line lib/promscrape/targetstatus.qtpl:285
+func writescrapeJobTargets(qq422016 qtio422016.Writer, num int, jts *jobTargetsStatuses) {
+//line lib/promscrape/targetstatus.qtpl:285
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:285
+	streamscrapeJobTargets(qw422016, num, jts)
+//line lib/promscrape/targetstatus.qtpl:285
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:285
+}
+
+//line lib/promscrape/targetstatus.qtpl:285
+func scrapeJobTargets(num int, jts *jobTargetsStatuses) string {
+//line lib/promscrape/targetstatus.qtpl:285
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:285
+	writescrapeJobTargets(qb422016, num, jts)
+//line lib/promscrape/targetstatus.qtpl:285
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:285
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:285
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:285
+}
+
+//line lib/promscrape/targetstatus.qtpl:287
+func streamdiscoveredTargets(qw422016 *qt422016.Writer, tsr *targetsStatusResult) {
+//line lib/promscrape/targetstatus.qtpl:288
+	tljs := tsr.getTargetLabelsByJob()
+
+//line lib/promscrape/targetstatus.qtpl:288
+	qw422016.N().S(`<div class="row mt-4"><div class="col-12">`)
+//line lib/promscrape/targetstatus.qtpl:291
+	for i, tlj := range tljs {
+//line lib/promscrape/targetstatus.qtpl:292
+		streamdiscoveredJobTargets(qw422016, i, tlj)
+//line lib/promscrape/targetstatus.qtpl:293
 	}
-//line lib/promscrape/targetstatus.qtpl:225
+//line lib/promscrape/targetstatus.qtpl:293
 	qw422016.N().S(`</div></div>`)
-//line lib/promscrape/targetstatus.qtpl:229
-	for _, jobName := range emptyJobs {
-//line lib/promscrape/targetstatus.qtpl:229
-		qw422016.N().S(`<div><h4><a>`)
-//line lib/promscrape/targetstatus.qtpl:232
-		qw422016.E().S(jobName)
-//line lib/promscrape/targetstatus.qtpl:232
-		qw422016.N().S(`(0/0 up)</a></h4><table class="table table-striped table-hover table-bordered table-sm"><thead><tr><th scope="col">Endpoint</th><th scope="col">State</th><th scope="col">Labels</th><th scope="col">Last Scrape</th><th scope="col">Scrape Duration</th><th scope="col">Samples Scraped</th><th scope="col">Error</th></tr></thead></table></div>`)
-//line lib/promscrape/targetstatus.qtpl:248
+//line lib/promscrape/targetstatus.qtpl:296
+}
+
+//line lib/promscrape/targetstatus.qtpl:296
+func writediscoveredTargets(qq422016 qtio422016.Writer, tsr *targetsStatusResult) {
+//line lib/promscrape/targetstatus.qtpl:296
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:296
+	streamdiscoveredTargets(qw422016, tsr)
+//line lib/promscrape/targetstatus.qtpl:296
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:296
+}
+
+//line lib/promscrape/targetstatus.qtpl:296
+func discoveredTargets(tsr *targetsStatusResult) string {
+//line lib/promscrape/targetstatus.qtpl:296
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:296
+	writediscoveredTargets(qb422016, tsr)
+//line lib/promscrape/targetstatus.qtpl:296
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:296
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:296
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:296
+}
+
+//line lib/promscrape/targetstatus.qtpl:298
+func streamdiscoveredJobTargets(qw422016 *qt422016.Writer, num int, tlj *targetLabelsByJob) {
+//line lib/promscrape/targetstatus.qtpl:298
+	qw422016.N().S(`<h4><span class="me-2">`)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.E().S(tlj.jobName)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().S(`(`)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().D(tlj.activeTargets)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().S(`/`)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().D(tlj.activeTargets + tlj.droppedTargets)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:300
+	qw422016.N().S(`active)</span>`)
+//line lib/promscrape/targetstatus.qtpl:301
+	streamshowHideScrapeJobButtons(qw422016, num)
+//line lib/promscrape/targetstatus.qtpl:301
+	qw422016.N().S(`</h4><div id="scrape-job-`)
+//line lib/promscrape/targetstatus.qtpl:303
+	qw422016.N().D(num)
+//line lib/promscrape/targetstatus.qtpl:303
+	qw422016.N().S(`" class="scrape-job table-responsive"><table class="table table-striped table-hover table-bordered table-sm"><thead><tr><th scope="col" style="width: 5%">Status</th><th scope="col" style="width: 65%">Discovered Labels</th><th scope="col" style="width: 30%">Target Labels</th></tr></thead><tbody>`)
+//line lib/promscrape/targetstatus.qtpl:313
+	for _, t := range tlj.targets {
+//line lib/promscrape/targetstatus.qtpl:313
+		qw422016.N().S(`<tr`)
+//line lib/promscrape/targetstatus.qtpl:315
+		if !t.up {
+//line lib/promscrape/targetstatus.qtpl:316
+			qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:316
+			qw422016.N().S(`role="alert"`)
+//line lib/promscrape/targetstatus.qtpl:316
+			qw422016.N().S(` `)
+//line lib/promscrape/targetstatus.qtpl:317
+			if len(t.labels) > 0 {
+//line lib/promscrape/targetstatus.qtpl:317
+				qw422016.N().S(`class="alert alert-danger"`)
+//line lib/promscrape/targetstatus.qtpl:319
+			} else {
+//line lib/promscrape/targetstatus.qtpl:319
+				qw422016.N().S(`class="alert alert-warning"`)
+//line lib/promscrape/targetstatus.qtpl:321
+			}
+//line lib/promscrape/targetstatus.qtpl:322
+		}
+//line lib/promscrape/targetstatus.qtpl:322
+		qw422016.N().S(`><td>`)
+//line lib/promscrape/targetstatus.qtpl:325
+		if t.up {
+//line lib/promscrape/targetstatus.qtpl:325
+			qw422016.N().S(`<span class="badge bg-success">UP</span>`)
+//line lib/promscrape/targetstatus.qtpl:327
+		} else if len(t.labels) > 0 {
+//line lib/promscrape/targetstatus.qtpl:327
+			qw422016.N().S(`<span class="badge bg-danger">DOWN</span>`)
+//line lib/promscrape/targetstatus.qtpl:329
+		} else {
+//line lib/promscrape/targetstatus.qtpl:329
+			qw422016.N().S(`<span class="badge bg-warning">DROPPED</span>`)
+//line lib/promscrape/targetstatus.qtpl:331
+		}
+//line lib/promscrape/targetstatus.qtpl:331
+		qw422016.N().S(`</td><td class="labels">`)
+//line lib/promscrape/targetstatus.qtpl:334
+		streamformatLabel(qw422016, t.discoveredLabels)
+//line lib/promscrape/targetstatus.qtpl:334
+		qw422016.N().S(`</td><td class="labels">`)
+//line lib/promscrape/targetstatus.qtpl:337
+		streamformatLabel(qw422016, promrelabel.FinalizeLabels(nil, t.labels))
+//line lib/promscrape/targetstatus.qtpl:337
+		qw422016.N().S(`</td></tr>`)
+//line lib/promscrape/targetstatus.qtpl:340
 	}
-//line lib/promscrape/targetstatus.qtpl:248
-	qw422016.N().S(`</main></div></div></body></html>`)
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:340
+	qw422016.N().S(`</tbody></table></div>`)
+//line lib/promscrape/targetstatus.qtpl:344
 }
 
-//line lib/promscrape/targetstatus.qtpl:254
-func WriteTargetsResponseHTML(qq422016 qtio422016.Writer, jts []jobTargetsStatuses, emptyJobs []string, showOnlyUnhealthy bool, endpointSearch, labelSearch string, err error) {
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
+func writediscoveredJobTargets(qq422016 qtio422016.Writer, num int, tlj *targetLabelsByJob) {
+//line lib/promscrape/targetstatus.qtpl:344
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line lib/promscrape/targetstatus.qtpl:254
-	StreamTargetsResponseHTML(qw422016, jts, emptyJobs, showOnlyUnhealthy, endpointSearch, labelSearch, err)
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
+	streamdiscoveredJobTargets(qw422016, num, tlj)
+//line lib/promscrape/targetstatus.qtpl:344
 	qt422016.ReleaseWriter(qw422016)
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
 }
 
-//line lib/promscrape/targetstatus.qtpl:254
-func TargetsResponseHTML(jts []jobTargetsStatuses, emptyJobs []string, showOnlyUnhealthy bool, endpointSearch, labelSearch string, err error) string {
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
+func discoveredJobTargets(num int, tlj *targetLabelsByJob) string {
+//line lib/promscrape/targetstatus.qtpl:344
 	qb422016 := qt422016.AcquireByteBuffer()
-//line lib/promscrape/targetstatus.qtpl:254
-	WriteTargetsResponseHTML(qb422016, jts, emptyJobs, showOnlyUnhealthy, endpointSearch, labelSearch, err)
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
+	writediscoveredJobTargets(qb422016, num, tlj)
+//line lib/promscrape/targetstatus.qtpl:344
 	qs422016 := string(qb422016.B)
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
 	qt422016.ReleaseByteBuffer(qb422016)
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
 	return qs422016
-//line lib/promscrape/targetstatus.qtpl:254
+//line lib/promscrape/targetstatus.qtpl:344
 }
 
-//line lib/promscrape/targetstatus.qtpl:256
-func streamqueryArgs(qw422016 *qt422016.Writer, m map[string]string) {
-//line lib/promscrape/targetstatus.qtpl:258
+//line lib/promscrape/targetstatus.qtpl:346
+func streamshowHideScrapeJobButtons(qw422016 *qt422016.Writer, num int) {
+//line lib/promscrape/targetstatus.qtpl:346
+	qw422016.N().S(`<button type="button" class="btn btn-primary btn-sm me-1"onclick="document.getElementById('scrape-job-`)
+//line lib/promscrape/targetstatus.qtpl:348
+	qw422016.N().D(num)
+//line lib/promscrape/targetstatus.qtpl:348
+	qw422016.N().S(`').style.display='none'">collapse</button><button type="button" class="btn btn-secondary btn-sm me-1"onclick="document.getElementById('scrape-job-`)
+//line lib/promscrape/targetstatus.qtpl:352
+	qw422016.N().D(num)
+//line lib/promscrape/targetstatus.qtpl:352
+	qw422016.N().S(`').style.display='block'">expand</button>`)
+//line lib/promscrape/targetstatus.qtpl:355
+}
+
+//line lib/promscrape/targetstatus.qtpl:355
+func writeshowHideScrapeJobButtons(qq422016 qtio422016.Writer, num int) {
+//line lib/promscrape/targetstatus.qtpl:355
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line lib/promscrape/targetstatus.qtpl:355
+	streamshowHideScrapeJobButtons(qw422016, num)
+//line lib/promscrape/targetstatus.qtpl:355
+	qt422016.ReleaseWriter(qw422016)
+//line lib/promscrape/targetstatus.qtpl:355
+}
+
+//line lib/promscrape/targetstatus.qtpl:355
+func showHideScrapeJobButtons(num int) string {
+//line lib/promscrape/targetstatus.qtpl:355
+	qb422016 := qt422016.AcquireByteBuffer()
+//line lib/promscrape/targetstatus.qtpl:355
+	writeshowHideScrapeJobButtons(qb422016, num)
+//line lib/promscrape/targetstatus.qtpl:355
+	qs422016 := string(qb422016.B)
+//line lib/promscrape/targetstatus.qtpl:355
+	qt422016.ReleaseByteBuffer(qb422016)
+//line lib/promscrape/targetstatus.qtpl:355
+	return qs422016
+//line lib/promscrape/targetstatus.qtpl:355
+}
+
+//line lib/promscrape/targetstatus.qtpl:357
+func streamqueryArgs(qw422016 *qt422016.Writer, filter *requestFilter, override map[string]string) {
+//line lib/promscrape/targetstatus.qtpl:359
+	showOnlyUnhealthy := "false"
+	if filter.showOnlyUnhealthy {
+		showOnlyUnhealthy = "true"
+	}
+	m := map[string]string{
+		"show_only_unhealthy": showOnlyUnhealthy,
+		"endpoint_search":     filter.endpointSearch,
+		"label_search":        filter.labelSearch,
+	}
+	for k, v := range override {
+		m[k] = v
+	}
 	qa := make(url.Values, len(m))
 	for k, v := range m {
 		qa[k] = []string{v}
 	}
 
-//line lib/promscrape/targetstatus.qtpl:263
+//line lib/promscrape/targetstatus.qtpl:376
 	qw422016.E().S(qa.Encode())
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
 }
 
-//line lib/promscrape/targetstatus.qtpl:264
-func writequeryArgs(qq422016 qtio422016.Writer, m map[string]string) {
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
+func writequeryArgs(qq422016 qtio422016.Writer, filter *requestFilter, override map[string]string) {
+//line lib/promscrape/targetstatus.qtpl:377
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line lib/promscrape/targetstatus.qtpl:264
-	streamqueryArgs(qw422016, m)
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
+	streamqueryArgs(qw422016, filter, override)
+//line lib/promscrape/targetstatus.qtpl:377
 	qt422016.ReleaseWriter(qw422016)
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
 }
 
-//line lib/promscrape/targetstatus.qtpl:264
-func queryArgs(m map[string]string) string {
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
+func queryArgs(filter *requestFilter, override map[string]string) string {
+//line lib/promscrape/targetstatus.qtpl:377
 	qb422016 := qt422016.AcquireByteBuffer()
-//line lib/promscrape/targetstatus.qtpl:264
-	writequeryArgs(qb422016, m)
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
+	writequeryArgs(qb422016, filter, override)
+//line lib/promscrape/targetstatus.qtpl:377
 	qs422016 := string(qb422016.B)
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
 	qt422016.ReleaseByteBuffer(qb422016)
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
 	return qs422016
-//line lib/promscrape/targetstatus.qtpl:264
+//line lib/promscrape/targetstatus.qtpl:377
 }
 
-//line lib/promscrape/targetstatus.qtpl:266
+//line lib/promscrape/targetstatus.qtpl:379
 func streamformatLabel(qw422016 *qt422016.Writer, labels []prompbmarshal.Label) {
-//line lib/promscrape/targetstatus.qtpl:266
+//line lib/promscrape/targetstatus.qtpl:379
 	qw422016.N().S(`{`)
-//line lib/promscrape/targetstatus.qtpl:268
+//line lib/promscrape/targetstatus.qtpl:381
 	for i, label := range labels {
-//line lib/promscrape/targetstatus.qtpl:269
+//line lib/promscrape/targetstatus.qtpl:382
 		qw422016.E().S(label.Name)
-//line lib/promscrape/targetstatus.qtpl:269
+//line lib/promscrape/targetstatus.qtpl:382
 		qw422016.N().S(`=`)
-//line lib/promscrape/targetstatus.qtpl:269
+//line lib/promscrape/targetstatus.qtpl:382
 		qw422016.E().Q(label.Value)
-//line lib/promscrape/targetstatus.qtpl:270
+//line lib/promscrape/targetstatus.qtpl:383
 		if i+1 < len(labels) {
-//line lib/promscrape/targetstatus.qtpl:270
+//line lib/promscrape/targetstatus.qtpl:383
 			qw422016.N().S(`,`)
-//line lib/promscrape/targetstatus.qtpl:270
+//line lib/promscrape/targetstatus.qtpl:383
 			qw422016.N().S(` `)
-//line lib/promscrape/targetstatus.qtpl:270
+//line lib/promscrape/targetstatus.qtpl:383
 		}
-//line lib/promscrape/targetstatus.qtpl:271
+//line lib/promscrape/targetstatus.qtpl:384
 	}
-//line lib/promscrape/targetstatus.qtpl:271
+//line lib/promscrape/targetstatus.qtpl:384
 	qw422016.N().S(`}`)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 }
 
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 func writeformatLabel(qq422016 qtio422016.Writer, labels []prompbmarshal.Label) {
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	streamformatLabel(qw422016, labels)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	qt422016.ReleaseWriter(qw422016)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 }
 
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 func formatLabel(labels []prompbmarshal.Label) string {
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	qb422016 := qt422016.AcquireByteBuffer()
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	writeformatLabel(qb422016, labels)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	qs422016 := string(qb422016.B)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	qt422016.ReleaseByteBuffer(qb422016)
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 	return qs422016
-//line lib/promscrape/targetstatus.qtpl:273
+//line lib/promscrape/targetstatus.qtpl:386
 }
 
-//line lib/promscrape/targetstatus.qtpl:275
+//line lib/promscrape/targetstatus.qtpl:388
 func streamerrorNotification(qw422016 *qt422016.Writer, err error) {
-//line lib/promscrape/targetstatus.qtpl:275
+//line lib/promscrape/targetstatus.qtpl:388
 	qw422016.N().S(`<div class="alert alert-danger d-flex align-items-center" role="alert"><svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Danger:"><use xlink:href="#exclamation-triangle-fill"/></svg><div>`)
-//line lib/promscrape/targetstatus.qtpl:280
+//line lib/promscrape/targetstatus.qtpl:393
 	qw422016.E().S(err.Error())
-//line lib/promscrape/targetstatus.qtpl:280
+//line lib/promscrape/targetstatus.qtpl:393
 	qw422016.N().S(`</div></div>`)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 }
 
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 func writeerrorNotification(qq422016 qtio422016.Writer, err error) {
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	streamerrorNotification(qw422016, err)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	qt422016.ReleaseWriter(qw422016)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 }
 
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 func errorNotification(err error) string {
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	qb422016 := qt422016.AcquireByteBuffer()
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	writeerrorNotification(qb422016, err)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	qs422016 := string(qb422016.B)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	qt422016.ReleaseByteBuffer(qb422016)
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 	return qs422016
-//line lib/promscrape/targetstatus.qtpl:283
+//line lib/promscrape/targetstatus.qtpl:396
 }
diff --git a/lib/storage/partition.go b/lib/storage/partition.go
index c61e281f1..ddd13fc48 100644
--- a/lib/storage/partition.go
+++ b/lib/storage/partition.go
@@ -869,10 +869,18 @@ func hasActiveMerges(pws []*partWrapper) bool {
 }
 
 var (
-	bigMergeWorkersCount   = (cgroup.AvailableCPUs() + 1) / 2
-	smallMergeWorkersCount = (cgroup.AvailableCPUs() + 1) / 2
+	bigMergeWorkersCount   = getDefaultMergeConcurrency(4)
+	smallMergeWorkersCount = getDefaultMergeConcurrency(8)
 )
 
+func getDefaultMergeConcurrency(max int) int {
+	v := (cgroup.AvailableCPUs() + 1) / 2
+	if v > max {
+		v = max
+	}
+	return v
+}
+
 // SetBigMergeWorkersCount sets the maximum number of concurrent mergers for big blocks.
 //
 // The function must be called before opening or creating any storage.
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go b/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
index 40db8fd32..beccfc7f6 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/endpoints/defaults.go
@@ -1937,9 +1937,15 @@ var awsPartition = partition{
 		},
 		"app-integrations": service{
 			Endpoints: serviceEndpoints{
+				endpointKey{
+					Region: "af-south-1",
+				}: endpoint{},
 				endpointKey{
 					Region: "ap-northeast-1",
 				}: endpoint{},
+				endpointKey{
+					Region: "ap-northeast-2",
+				}: endpoint{},
 				endpointKey{
 					Region: "ap-southeast-1",
 				}: endpoint{},
diff --git a/vendor/github.com/aws/aws-sdk-go/aws/version.go b/vendor/github.com/aws/aws-sdk-go/aws/version.go
index 6ef78c5c6..6fac339ed 100644
--- a/vendor/github.com/aws/aws-sdk-go/aws/version.go
+++ b/vendor/github.com/aws/aws-sdk-go/aws/version.go
@@ -5,4 +5,4 @@ package aws
 const SDKName = "aws-sdk-go"
 
 // SDKVersion is the version of this SDK
-const SDKVersion = "1.44.24"
+const SDKVersion = "1.44.27"
diff --git a/vendor/github.com/klauspost/compress/README.md b/vendor/github.com/klauspost/compress/README.md
index 615c34e13..5c3c2a258 100644
--- a/vendor/github.com/klauspost/compress/README.md
+++ b/vendor/github.com/klauspost/compress/README.md
@@ -17,6 +17,18 @@ This package provides various compression algorithms.
 
 # changelog
 
+* May 25, 2022 (v1.15.5)
+	* s2: Add concurrent stream decompression https://github.com/klauspost/compress/pull/602
+	* s2: Fix final emit oob read crash on amd64 https://github.com/klauspost/compress/pull/601
+	* huff0: asm implementation of Decompress1X by @WojciechMula https://github.com/klauspost/compress/pull/596
+	* zstd: Use 1 less goroutine for stream decoding https://github.com/klauspost/compress/pull/588
+	* zstd: Copy literal in 16 byte blocks when possible https://github.com/klauspost/compress/pull/592
+	* zstd: Speed up when WithDecoderLowmem(false) https://github.com/klauspost/compress/pull/599
+	* zstd: faster next state update in BMI2 version of decode by @WojciechMula in https://github.com/klauspost/compress/pull/593
+	* huff0: Do not check max size when reading table. https://github.com/klauspost/compress/pull/586
+	* flate: Inplace hashing for level 7-9 by @klauspost in https://github.com/klauspost/compress/pull/590
+
+
 * May 11, 2022 (v1.15.4)
 	* huff0: decompress directly into output by @WojciechMula in [#577](https://github.com/klauspost/compress/pull/577)
 	* inflate: Keep dict on stack [#581](https://github.com/klauspost/compress/pull/581)
@@ -83,6 +95,9 @@ While the release has been extensively tested, it is recommended to testing when
 	* zstd: add arm64 xxhash assembly in [#464](https://github.com/klauspost/compress/pull/464)
 	* Add garbled for binaries for s2 in [#445](https://github.com/klauspost/compress/pull/445)
 
+<details>
+	<summary>See changes to v1.13.x</summary>
+	
 * Aug 30, 2021 (v1.13.5)
 	* gz/zlib/flate: Alias stdlib errors [#425](https://github.com/klauspost/compress/pull/425)
 	* s2: Add block support to commandline tools [#413](https://github.com/klauspost/compress/pull/413)
@@ -111,6 +126,8 @@ While the release has been extensively tested, it is recommended to testing when
 	* Added [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp#gzip-handler) which allows wrapping HTTP servers and clients with GZIP compressors.
 	* zstd: Detect short invalid signatures [#382](https://github.com/klauspost/compress/pull/382)
 	* zstd: Spawn decoder goroutine only if needed. [#380](https://github.com/klauspost/compress/pull/380)
+</details>
+
 
 <details>
 	<summary>See changes to v1.12.x</summary>
diff --git a/vendor/github.com/klauspost/compress/zstd/enc_better.go b/vendor/github.com/klauspost/compress/zstd/enc_better.go
index 602c05ee0..c769f6941 100644
--- a/vendor/github.com/klauspost/compress/zstd/enc_better.go
+++ b/vendor/github.com/klauspost/compress/zstd/enc_better.go
@@ -156,8 +156,8 @@ encodeLoop:
 				panic("offset0 was 0")
 			}
 
-			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 			nextHashL := hashLen(cv, betterLongTableBits, betterLongLen)
+			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 			candidateL := e.longTable[nextHashL]
 			candidateS := e.table[nextHashS]
 
@@ -518,8 +518,8 @@ encodeLoop:
 			}
 
 			// Store this, since we have it.
-			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 			nextHashL := hashLen(cv, betterLongTableBits, betterLongLen)
+			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 
 			// We have at least 4 byte match.
 			// No need to check backwards. We come straight from a match
@@ -674,8 +674,8 @@ encodeLoop:
 				panic("offset0 was 0")
 			}
 
-			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 			nextHashL := hashLen(cv, betterLongTableBits, betterLongLen)
+			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 			candidateL := e.longTable[nextHashL]
 			candidateS := e.table[nextHashS]
 
@@ -1047,8 +1047,8 @@ encodeLoop:
 			}
 
 			// Store this, since we have it.
-			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 			nextHashL := hashLen(cv, betterLongTableBits, betterLongLen)
+			nextHashS := hashLen(cv, betterShortTableBits, betterShortLen)
 
 			// We have at least 4 byte match.
 			// No need to check backwards. We come straight from a match
diff --git a/vendor/github.com/klauspost/compress/zstd/enc_dfast.go b/vendor/github.com/klauspost/compress/zstd/enc_dfast.go
index d6b310424..7ff0c64fa 100644
--- a/vendor/github.com/klauspost/compress/zstd/enc_dfast.go
+++ b/vendor/github.com/klauspost/compress/zstd/enc_dfast.go
@@ -127,8 +127,8 @@ encodeLoop:
 				panic("offset0 was 0")
 			}
 
-			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			nextHashL := hashLen(cv, dFastLongTableBits, dFastLongLen)
+			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			candidateL := e.longTable[nextHashL]
 			candidateS := e.table[nextHashS]
 
@@ -439,8 +439,8 @@ encodeLoop:
 		var t int32
 		for {
 
-			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			nextHashL := hashLen(cv, dFastLongTableBits, dFastLongLen)
+			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			candidateL := e.longTable[nextHashL]
 			candidateS := e.table[nextHashS]
 
@@ -785,8 +785,8 @@ encodeLoop:
 				panic("offset0 was 0")
 			}
 
-			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			nextHashL := hashLen(cv, dFastLongTableBits, dFastLongLen)
+			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			candidateL := e.longTable[nextHashL]
 			candidateS := e.table[nextHashS]
 
@@ -969,7 +969,7 @@ encodeLoop:
 		te0 := tableEntry{offset: index0 + e.cur, val: uint32(cv0)}
 		te1 := tableEntry{offset: index1 + e.cur, val: uint32(cv1)}
 		longHash1 := hashLen(cv0, dFastLongTableBits, dFastLongLen)
-		longHash2 := hashLen(cv0, dFastLongTableBits, dFastLongLen)
+		longHash2 := hashLen(cv1, dFastLongTableBits, dFastLongLen)
 		e.longTable[longHash1] = te0
 		e.longTable[longHash2] = te1
 		e.markLongShardDirty(longHash1)
@@ -1002,8 +1002,8 @@ encodeLoop:
 			}
 
 			// Store this, since we have it.
-			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 			nextHashL := hashLen(cv, dFastLongTableBits, dFastLongLen)
+			nextHashS := hashLen(cv, dFastShortTableBits, dFastShortLen)
 
 			// We have at least 4 byte match.
 			// No need to check backwards. We come straight from a match
diff --git a/vendor/github.com/klauspost/compress/zstd/encoder.go b/vendor/github.com/klauspost/compress/zstd/encoder.go
index dcc987a7c..e6b1d01cf 100644
--- a/vendor/github.com/klauspost/compress/zstd/encoder.go
+++ b/vendor/github.com/klauspost/compress/zstd/encoder.go
@@ -551,7 +551,7 @@ func (e *Encoder) EncodeAll(src, dst []byte) []byte {
 	}
 
 	// If we can do everything in one block, prefer that.
-	if len(src) <= maxCompressedBlockSize {
+	if len(src) <= e.o.blockSize {
 		enc.Reset(e.o.dict, true)
 		// Slightly faster with no history and everything in one block.
 		if e.o.crc {
diff --git a/vendor/github.com/klauspost/compress/zstd/zip.go b/vendor/github.com/klauspost/compress/zstd/zip.go
index b53f606a1..29c15c8c4 100644
--- a/vendor/github.com/klauspost/compress/zstd/zip.go
+++ b/vendor/github.com/klauspost/compress/zstd/zip.go
@@ -18,7 +18,14 @@ const ZipMethodWinZip = 93
 // See https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.9.TXT
 const ZipMethodPKWare = 20
 
-var zipReaderPool sync.Pool
+// zipReaderPool is the default reader pool.
+var zipReaderPool = sync.Pool{New: func() interface{} {
+	z, err := NewReader(nil, WithDecoderLowmem(true), WithDecoderMaxWindow(128<<20), WithDecoderConcurrency(1))
+	if err != nil {
+		panic(err)
+	}
+	return z
+}}
 
 // newZipReader creates a pooled zip decompressor.
 func newZipReader(opts ...DOption) func(r io.Reader) io.ReadCloser {
diff --git a/vendor/golang.org/x/net/http2/writesched_priority.go b/vendor/golang.org/x/net/http2/writesched_priority.go
index 2618b2c11..0a242c669 100644
--- a/vendor/golang.org/x/net/http2/writesched_priority.go
+++ b/vendor/golang.org/x/net/http2/writesched_priority.go
@@ -383,16 +383,15 @@ func (ws *priorityWriteScheduler) AdjustStream(streamID uint32, priority Priorit
 
 func (ws *priorityWriteScheduler) Push(wr FrameWriteRequest) {
 	var n *priorityNode
-	if id := wr.StreamID(); id == 0 {
+	if wr.isControl() {
 		n = &ws.root
 	} else {
+		id := wr.StreamID()
 		n = ws.nodes[id]
 		if n == nil {
 			// id is an idle or closed stream. wr should not be a HEADERS or
-			// DATA frame. However, wr can be a RST_STREAM. In this case, we
-			// push wr onto the root, rather than creating a new priorityNode,
-			// since RST_STREAM is tiny and the stream's priority is unknown
-			// anyway. See issue #17919.
+			// DATA frame. In other case, we push wr onto the root, rather
+			// than creating a new priorityNode.
 			if wr.DataSize() > 0 {
 				panic("add DATA on non-open stream")
 			}
diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go
index 1eab2fdf9..4c0850a45 100644
--- a/vendor/golang.org/x/sync/errgroup/errgroup.go
+++ b/vendor/golang.org/x/sync/errgroup/errgroup.go
@@ -17,7 +17,8 @@ type token struct{}
 // A Group is a collection of goroutines working on subtasks that are part of
 // the same overall task.
 //
-// A zero Group is valid and does not cancel on error.
+// A zero Group is valid, has no limit on the number of active goroutines,
+// and does not cancel on error.
 type Group struct {
 	cancel func()
 
diff --git a/vendor/google.golang.org/api/internal/version.go b/vendor/google.golang.org/api/internal/version.go
index 060319e1b..a382d160a 100644
--- a/vendor/google.golang.org/api/internal/version.go
+++ b/vendor/google.golang.org/api/internal/version.go
@@ -5,4 +5,4 @@
 package internal
 
 // Version is the current tagged release of the library.
-const Version = "0.81.0"
+const Version = "0.82.0"
diff --git a/vendor/google.golang.org/grpc/clientconn.go b/vendor/google.golang.org/grpc/clientconn.go
index ea9836d28..de6d41c23 100644
--- a/vendor/google.golang.org/grpc/clientconn.go
+++ b/vendor/google.golang.org/grpc/clientconn.go
@@ -801,16 +801,31 @@ func (ac *addrConn) connect() error {
 	return nil
 }
 
+func equalAddresses(a, b []resolver.Address) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i, v := range a {
+		if !v.Equal(b[i]) {
+			return false
+		}
+	}
+	return true
+}
+
 // tryUpdateAddrs tries to update ac.addrs with the new addresses list.
 //
-// If ac is Connecting, it returns false. The caller should tear down the ac and
-// create a new one. Note that the backoff will be reset when this happens.
-//
 // If ac is TransientFailure, it updates ac.addrs and returns true. The updated
 // addresses will be picked up by retry in the next iteration after backoff.
 //
 // If ac is Shutdown or Idle, it updates ac.addrs and returns true.
 //
+// If the addresses is the same as the old list, it does nothing and returns
+// true.
+//
+// If ac is Connecting, it returns false. The caller should tear down the ac and
+// create a new one. Note that the backoff will be reset when this happens.
+//
 // If ac is Ready, it checks whether current connected address of ac is in the
 // new addrs list.
 //  - If true, it updates ac.addrs and returns true. The ac will keep using
@@ -827,6 +842,10 @@ func (ac *addrConn) tryUpdateAddrs(addrs []resolver.Address) bool {
 		return true
 	}
 
+	if equalAddresses(ac.addrs, addrs) {
+		return true
+	}
+
 	if ac.state == connectivity.Connecting {
 		return false
 	}
@@ -1219,6 +1238,7 @@ func (ac *addrConn) createTransport(addr resolver.Address, copts transport.Conne
 		ac.mu.Lock()
 		defer ac.mu.Unlock()
 		defer connClosed.Fire()
+		defer hcancel()
 		if !hcStarted || hctx.Err() != nil {
 			// We didn't start the health check or set the state to READY, so
 			// no need to do anything else here.
@@ -1229,7 +1249,6 @@ func (ac *addrConn) createTransport(addr resolver.Address, copts transport.Conne
 			// state, since there may be a new transport in this addrConn.
 			return
 		}
-		hcancel()
 		ac.transport = nil
 		// Refresh the name resolver
 		ac.cc.resolveNow(resolver.ResolveNowOptions{})
@@ -1252,6 +1271,7 @@ func (ac *addrConn) createTransport(addr resolver.Address, copts transport.Conne
 	newTr, err := transport.NewClientTransport(connectCtx, ac.cc.ctx, addr, copts, func() { prefaceReceived.Fire() }, onGoAway, onClose)
 	if err != nil {
 		// newTr is either nil, or closed.
+		hcancel()
 		channelz.Warningf(logger, ac.channelzID, "grpc: addrConn.createTransport failed to connect to %s. Err: %v", addr, err)
 		return err
 	}
diff --git a/vendor/google.golang.org/grpc/encoding/encoding.go b/vendor/google.golang.org/grpc/encoding/encoding.go
index 6d84f74c7..18e530fc9 100644
--- a/vendor/google.golang.org/grpc/encoding/encoding.go
+++ b/vendor/google.golang.org/grpc/encoding/encoding.go
@@ -108,7 +108,7 @@ var registeredCodecs = make(map[string]Codec)
 // more details.
 //
 // NOTE: this function must only be called during initialization time (i.e. in
-// an init() function), and is not thread-safe.  If multiple Compressors are
+// an init() function), and is not thread-safe.  If multiple Codecs are
 // registered with the same name, the one registered last will take effect.
 func RegisterCodec(codec Codec) {
 	if codec == nil {
diff --git a/vendor/google.golang.org/grpc/internal/transport/controlbuf.go b/vendor/google.golang.org/grpc/internal/transport/controlbuf.go
index 8394d252d..244f4b081 100644
--- a/vendor/google.golang.org/grpc/internal/transport/controlbuf.go
+++ b/vendor/google.golang.org/grpc/internal/transport/controlbuf.go
@@ -137,6 +137,7 @@ type earlyAbortStream struct {
 	streamID       uint32
 	contentSubtype string
 	status         *status.Status
+	rst            bool
 }
 
 func (*earlyAbortStream) isTransportResponseFrame() bool { return false }
@@ -786,6 +787,11 @@ func (l *loopyWriter) earlyAbortStreamHandler(eas *earlyAbortStream) error {
 	if err := l.writeHeader(eas.streamID, true, headerFields, nil); err != nil {
 		return err
 	}
+	if eas.rst {
+		if err := l.framer.fr.WriteRSTStream(eas.streamID, http2.ErrCodeNo); err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
diff --git a/vendor/google.golang.org/grpc/internal/transport/http2_server.go b/vendor/google.golang.org/grpc/internal/transport/http2_server.go
index 0956b500c..45d7bd145 100644
--- a/vendor/google.golang.org/grpc/internal/transport/http2_server.go
+++ b/vendor/google.golang.org/grpc/internal/transport/http2_server.go
@@ -21,7 +21,6 @@ package transport
 import (
 	"bytes"
 	"context"
-	"errors"
 	"fmt"
 	"io"
 	"math"
@@ -53,10 +52,10 @@ import (
 var (
 	// ErrIllegalHeaderWrite indicates that setting header is illegal because of
 	// the stream's state.
-	ErrIllegalHeaderWrite = errors.New("transport: the stream is done or WriteHeader was already called")
+	ErrIllegalHeaderWrite = status.Error(codes.Internal, "transport: SendHeader called multiple times")
 	// ErrHeaderListSizeLimitViolation indicates that the header list size is larger
 	// than the limit set by peer.
-	ErrHeaderListSizeLimitViolation = errors.New("transport: trying to send header list size larger than the limit set by peer")
+	ErrHeaderListSizeLimitViolation = status.Error(codes.Internal, "transport: trying to send header list size larger than the limit set by peer")
 )
 
 // serverConnectionCounter counts the number of connections a server has seen
@@ -449,6 +448,7 @@ func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(
 			streamID:       streamID,
 			contentSubtype: s.contentSubtype,
 			status:         status.New(codes.Internal, errMsg),
+			rst:            !frame.StreamEnded(),
 		})
 		return false
 	}
@@ -522,14 +522,16 @@ func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(
 	}
 	if httpMethod != http.MethodPost {
 		t.mu.Unlock()
+		errMsg := fmt.Sprintf("http2Server.operateHeaders parsed a :method field: %v which should be POST", httpMethod)
 		if logger.V(logLevel) {
-			logger.Infof("transport: http2Server.operateHeaders parsed a :method field: %v which should be POST", httpMethod)
+			logger.Infof("transport: %v", errMsg)
 		}
-		t.controlBuf.put(&cleanupStream{
-			streamID: streamID,
-			rst:      true,
-			rstCode:  http2.ErrCodeProtocol,
-			onWrite:  func() {},
+		t.controlBuf.put(&earlyAbortStream{
+			httpStatus:     405,
+			streamID:       streamID,
+			contentSubtype: s.contentSubtype,
+			status:         status.New(codes.Internal, errMsg),
+			rst:            !frame.StreamEnded(),
 		})
 		s.cancel()
 		return false
@@ -550,6 +552,7 @@ func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(
 				streamID:       s.id,
 				contentSubtype: s.contentSubtype,
 				status:         stat,
+				rst:            !frame.StreamEnded(),
 			})
 			return false
 		}
@@ -931,11 +934,25 @@ func (t *http2Server) checkForHeaderListSize(it interface{}) bool {
 	return true
 }
 
+func (t *http2Server) streamContextErr(s *Stream) error {
+	select {
+	case <-t.done:
+		return ErrConnClosing
+	default:
+	}
+	return ContextErr(s.ctx.Err())
+}
+
 // WriteHeader sends the header metadata md back to the client.
 func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error {
-	if s.updateHeaderSent() || s.getState() == streamDone {
+	if s.updateHeaderSent() {
 		return ErrIllegalHeaderWrite
 	}
+
+	if s.getState() == streamDone {
+		return t.streamContextErr(s)
+	}
+
 	s.hdrMu.Lock()
 	if md.Len() > 0 {
 		if s.header.Len() > 0 {
@@ -946,7 +963,7 @@ func (t *http2Server) WriteHeader(s *Stream, md metadata.MD) error {
 	}
 	if err := t.writeHeaderLocked(s); err != nil {
 		s.hdrMu.Unlock()
-		return err
+		return status.Convert(err).Err()
 	}
 	s.hdrMu.Unlock()
 	return nil
@@ -1062,23 +1079,12 @@ func (t *http2Server) WriteStatus(s *Stream, st *status.Status) error {
 func (t *http2Server) Write(s *Stream, hdr []byte, data []byte, opts *Options) error {
 	if !s.isHeaderSent() { // Headers haven't been written yet.
 		if err := t.WriteHeader(s, nil); err != nil {
-			if _, ok := err.(ConnectionError); ok {
-				return err
-			}
-			// TODO(mmukhi, dfawley): Make sure this is the right code to return.
-			return status.Errorf(codes.Internal, "transport: %v", err)
+			return err
 		}
 	} else {
 		// Writing headers checks for this condition.
 		if s.getState() == streamDone {
-			// TODO(mmukhi, dfawley): Should the server write also return io.EOF?
-			s.cancel()
-			select {
-			case <-t.done:
-				return ErrConnClosing
-			default:
-			}
-			return ContextErr(s.ctx.Err())
+			return t.streamContextErr(s)
 		}
 	}
 	df := &dataFrame{
@@ -1088,12 +1094,7 @@ func (t *http2Server) Write(s *Stream, hdr []byte, data []byte, opts *Options) e
 		onEachWrite: t.setResetPingStrikes,
 	}
 	if err := s.wq.get(int32(len(hdr) + len(data))); err != nil {
-		select {
-		case <-t.done:
-			return ErrConnClosing
-		default:
-		}
-		return ContextErr(s.ctx.Err())
+		return t.streamContextErr(s)
 	}
 	return t.controlBuf.put(df)
 }
@@ -1229,10 +1230,6 @@ func (t *http2Server) Close() {
 
 // deleteStream deletes the stream s from transport's active streams.
 func (t *http2Server) deleteStream(s *Stream, eosReceived bool) {
-	// In case stream sending and receiving are invoked in separate
-	// goroutines (e.g., bi-directional streaming), cancel needs to be
-	// called to interrupt the potential blocking on other goroutines.
-	s.cancel()
 
 	t.mu.Lock()
 	if _, ok := t.activeStreams[s.id]; ok {
@@ -1254,6 +1251,11 @@ func (t *http2Server) deleteStream(s *Stream, eosReceived bool) {
 
 // finishStream closes the stream and puts the trailing headerFrame into controlbuf.
 func (t *http2Server) finishStream(s *Stream, rst bool, rstCode http2.ErrCode, hdr *headerFrame, eosReceived bool) {
+	// In case stream sending and receiving are invoked in separate
+	// goroutines (e.g., bi-directional streaming), cancel needs to be
+	// called to interrupt the potential blocking on other goroutines.
+	s.cancel()
+
 	oldState := s.swapState(streamDone)
 	if oldState == streamDone {
 		// If the stream was already done, return.
@@ -1273,6 +1275,11 @@ func (t *http2Server) finishStream(s *Stream, rst bool, rstCode http2.ErrCode, h
 
 // closeStream clears the footprint of a stream when the stream is not needed any more.
 func (t *http2Server) closeStream(s *Stream, rst bool, rstCode http2.ErrCode, eosReceived bool) {
+	// In case stream sending and receiving are invoked in separate
+	// goroutines (e.g., bi-directional streaming), cancel needs to be
+	// called to interrupt the potential blocking on other goroutines.
+	s.cancel()
+
 	s.swapState(streamDone)
 	t.deleteStream(s, eosReceived)
 
diff --git a/vendor/google.golang.org/grpc/server.go b/vendor/google.golang.org/grpc/server.go
index 96431a058..65de84b30 100644
--- a/vendor/google.golang.org/grpc/server.go
+++ b/vendor/google.golang.org/grpc/server.go
@@ -1801,12 +1801,26 @@ func (s *Server) getCodec(contentSubtype string) baseCodec {
 	return codec
 }
 
-// SetHeader sets the header metadata.
-// When called multiple times, all the provided metadata will be merged.
-// All the metadata will be sent out when one of the following happens:
-//  - grpc.SendHeader() is called;
-//  - The first response is sent out;
-//  - An RPC status is sent out (error or success).
+// SetHeader sets the header metadata to be sent from the server to the client.
+// The context provided must be the context passed to the server's handler.
+//
+// Streaming RPCs should prefer the SetHeader method of the ServerStream.
+//
+// When called multiple times, all the provided metadata will be merged.  All
+// the metadata will be sent out when one of the following happens:
+//
+// - grpc.SendHeader is called, or for streaming handlers, stream.SendHeader.
+// - The first response message is sent.  For unary handlers, this occurs when
+//   the handler returns; for streaming handlers, this can happen when stream's
+//   SendMsg method is called.
+// - An RPC status is sent out (error or success).  This occurs when the handler
+//   returns.
+//
+// SetHeader will fail if called after any of the events above.
+//
+// The error returned is compatible with the status package.  However, the
+// status code will often not match the RPC status as seen by the client
+// application, and therefore, should not be relied upon for this purpose.
 func SetHeader(ctx context.Context, md metadata.MD) error {
 	if md.Len() == 0 {
 		return nil
@@ -1818,8 +1832,14 @@ func SetHeader(ctx context.Context, md metadata.MD) error {
 	return stream.SetHeader(md)
 }
 
-// SendHeader sends header metadata. It may be called at most once.
-// The provided md and headers set by SetHeader() will be sent.
+// SendHeader sends header metadata. It may be called at most once, and may not
+// be called after any event that causes headers to be sent (see SetHeader for
+// a complete list).  The provided md and headers set by SetHeader() will be
+// sent.
+//
+// The error returned is compatible with the status package.  However, the
+// status code will often not match the RPC status as seen by the client
+// application, and therefore, should not be relied upon for this purpose.
 func SendHeader(ctx context.Context, md metadata.MD) error {
 	stream := ServerTransportStreamFromContext(ctx)
 	if stream == nil {
@@ -1833,6 +1853,10 @@ func SendHeader(ctx context.Context, md metadata.MD) error {
 
 // SetTrailer sets the trailer metadata that will be sent when an RPC returns.
 // When called more than once, all the provided metadata will be merged.
+//
+// The error returned is compatible with the status package.  However, the
+// status code will often not match the RPC status as seen by the client
+// application, and therefore, should not be relied upon for this purpose.
 func SetTrailer(ctx context.Context, md metadata.MD) error {
 	if md.Len() == 0 {
 		return nil
diff --git a/vendor/google.golang.org/grpc/stream.go b/vendor/google.golang.org/grpc/stream.go
index be5140535..236fc17ec 100644
--- a/vendor/google.golang.org/grpc/stream.go
+++ b/vendor/google.golang.org/grpc/stream.go
@@ -1381,8 +1381,10 @@ func (as *addrConnStream) finish(err error) {
 
 // ServerStream defines the server-side behavior of a streaming RPC.
 //
-// All errors returned from ServerStream methods are compatible with the
-// status package.
+// Errors returned from ServerStream methods are compatible with the status
+// package.  However, the status code will often not match the RPC status as
+// seen by the client application, and therefore, should not be relied upon for
+// this purpose.
 type ServerStream interface {
 	// SetHeader sets the header metadata. It may be called multiple times.
 	// When call multiple times, all the provided metadata will be merged.
diff --git a/vendor/google.golang.org/grpc/version.go b/vendor/google.golang.org/grpc/version.go
index f17192e6a..5bc03f9b3 100644
--- a/vendor/google.golang.org/grpc/version.go
+++ b/vendor/google.golang.org/grpc/version.go
@@ -19,4 +19,4 @@
 package grpc
 
 // Version is the current grpc version.
-const Version = "1.46.2"
+const Version = "1.47.0"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index cb2c654c4..e19d8e5f9 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -34,7 +34,7 @@ github.com/VictoriaMetrics/metricsql/binaryop
 # github.com/VividCortex/ewma v1.2.0
 ## explicit; go 1.12
 github.com/VividCortex/ewma
-# github.com/aws/aws-sdk-go v1.44.24
+# github.com/aws/aws-sdk-go v1.44.27
 ## explicit; go 1.11
 github.com/aws/aws-sdk-go/aws
 github.com/aws/aws-sdk-go/aws/arn
@@ -155,7 +155,7 @@ github.com/influxdata/influxdb/pkg/escape
 # github.com/jmespath/go-jmespath v0.4.0
 ## explicit; go 1.14
 github.com/jmespath/go-jmespath
-# github.com/klauspost/compress v1.15.5
+# github.com/klauspost/compress v1.15.6
 ## explicit; go 1.16
 github.com/klauspost/compress
 github.com/klauspost/compress/flate
@@ -277,7 +277,7 @@ go.opencensus.io/trace/tracestate
 go.uber.org/atomic
 # go.uber.org/goleak v1.1.11-0.20210813005559-691160354723
 ## explicit; go 1.13
-# golang.org/x/net v0.0.0-20220526153639-5463443f8c37
+# golang.org/x/net v0.0.0-20220531201128-c960675eff93
 ## explicit; go 1.17
 golang.org/x/net/context
 golang.org/x/net/context/ctxhttp
@@ -299,7 +299,7 @@ golang.org/x/oauth2/google/internal/externalaccount
 golang.org/x/oauth2/internal
 golang.org/x/oauth2/jws
 golang.org/x/oauth2/jwt
-# golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
+# golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
 ## explicit
 golang.org/x/sync/errgroup
 # golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
@@ -317,7 +317,7 @@ golang.org/x/text/unicode/norm
 ## explicit; go 1.17
 golang.org/x/xerrors
 golang.org/x/xerrors/internal
-# google.golang.org/api v0.81.0
+# google.golang.org/api v0.82.0
 ## explicit; go 1.15
 google.golang.org/api/googleapi
 google.golang.org/api/googleapi/transport
@@ -350,7 +350,7 @@ google.golang.org/appengine/internal/socket
 google.golang.org/appengine/internal/urlfetch
 google.golang.org/appengine/socket
 google.golang.org/appengine/urlfetch
-# google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58
+# google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8
 ## explicit; go 1.15
 google.golang.org/genproto/googleapis/api/annotations
 google.golang.org/genproto/googleapis/iam/v1
@@ -365,7 +365,7 @@ google.golang.org/genproto/googleapis/type/decimal
 google.golang.org/genproto/googleapis/type/expr
 google.golang.org/genproto/googleapis/type/fraction
 google.golang.org/genproto/googleapis/type/month
-# google.golang.org/grpc v1.46.2
+# google.golang.org/grpc v1.47.0
 ## explicit; go 1.14
 google.golang.org/grpc
 google.golang.org/grpc/attributes


Test patch:
diff --git a/lib/promscrape/discovery/kubernetes/kubeconfig_test.go b/lib/promscrape/discovery/kubernetes/kubeconfig_test.go
index cb50ff94c..fc0c7334e 100644
--- a/lib/promscrape/discovery/kubernetes/kubeconfig_test.go
+++ b/lib/promscrape/discovery/kubernetes/kubeconfig_test.go
@@ -11,26 +11,22 @@ func TestParseKubeConfigSuccess(t *testing.T) {
 
 	type testCase struct {
 		name           string
-		sdc            *SDConfig
+		kubeConfigFile string
 		expectedConfig *kubeConfig
 	}
 
 	var cases = []testCase{
 		{
-			name: "token",
-			sdc: &SDConfig{
-				KubeConfig: "testdata/good_kubeconfig/with_token.yaml",
-			},
+			name:           "token",
+			kubeConfigFile: "testdata/good_kubeconfig/with_token.yaml",
 			expectedConfig: &kubeConfig{
 				server: "http://some-server:8080",
 				token:  "abc",
 			},
 		},
 		{
-			name: "cert",
-			sdc: &SDConfig{
-				KubeConfig: "testdata/good_kubeconfig/with_tls.yaml",
-			},
+			name:           "cert",
+			kubeConfigFile: "testdata/good_kubeconfig/with_tls.yaml",
 			expectedConfig: &kubeConfig{
 				server: "https://localhost:6443",
 				tlsConfig: &promauth.TLSConfig{
@@ -41,10 +37,8 @@ func TestParseKubeConfigSuccess(t *testing.T) {
 			},
 		},
 		{
-			name: "basic",
-			sdc: &SDConfig{
-				KubeConfig: "testdata/good_kubeconfig/with_basic.yaml",
-			},
+			name:           "basic",
+			kubeConfigFile: "testdata/good_kubeconfig/with_basic.yaml",
 			expectedConfig: &kubeConfig{
 				server: "http://some-server:8080",
 				basicAuth: &promauth.BasicAuthConfig{
@@ -56,7 +50,7 @@ func TestParseKubeConfigSuccess(t *testing.T) {
 	}
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			ac, err := buildConfig(tc.sdc)
+			ac, err := newKubeConfig(tc.kubeConfigFile)
 			if err != nil {
 				t.Fatalf("unexpected error: %v", err)
 			}
@@ -68,14 +62,11 @@ func TestParseKubeConfigSuccess(t *testing.T) {
 }
 
 func TestParseKubeConfigFail(t *testing.T) {
-	f := func(name, kubeConfigPath string) {
+	f := func(name, kubeConfigFile string) {
 		t.Helper()
 		t.Run(name, func(t *testing.T) {
-			sdc := &SDConfig{
-				KubeConfig: kubeConfigPath,
-			}
-			if _, err := buildConfig(sdc); err == nil {
-				t.Fatalf("unexpected result for config file: %s, must return error", kubeConfigPath)
+			if _, err := newKubeConfig(kubeConfigFile); err == nil {
+				t.Fatalf("unexpected result for config file: %s, must return error", kubeConfigFile)
 			}
 		})
 	}