Repository: VictoriaMetrics/VictoriaMetrics
Pull request #759 (2020-09-09 10:13:41)

vmalert: update groups on config reload only if changes detected
On config reload event `vmalert` reloads configuration for every group. While
it works for simple configurations, the more complex and heavy installations may
suffer from frequent config reloads.
The change introduces the `checksum` field for every group and is set to md5 hash
of yaml configuration. The checksum will change if on any change to group
definition like rules order or annotation change. Comparing the `checksum` field
on config reload event helps to detect if group should be updated.
The groups update is now done concurrently, so reload duration will be limited by
the slowest group.

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=h1) Report
> Merging [#759](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=desc) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/df169b1ebd2b73f1ece5ce17e46e7682fdfbf3f1?el=desc) will **decrease** coverage by `0.08%`.
> The diff coverage is `61.53%`.

[![Impacted file tree graph](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/graphs/tree.svg?width=650&height=150&src=pr&token=YND8dYVcXn)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #759      +/-   ##
==========================================
- Coverage   68.19%   68.10%   -0.09%     
==========================================
  Files         164      164              
  Lines       22240    22270      +30     
==========================================
+ Hits        15167    15168       +1     
- Misses       5838     5864      +26     
- Partials     1235     1238       +3     
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [app/vmalert/manager.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvbWFuYWdlci5nbw==) | `73.25% <35.29%> (-9.61%)` | :arrow_down: |
| [app/vmalert/config/config.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvY29uZmlnL2NvbmZpZy5nbw==) | `78.81% <80.00%> (-1.38%)` | :arrow_down: |
| [app/vmalert/group.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvZ3JvdXAuZ28=) | `67.34% <100.00%> (-2.25%)` | :arrow_down: |
| [lib/fasttime/fasttime.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-bGliL2Zhc3R0aW1lL2Zhc3R0aW1lLmdv) | `53.84% <0.00%> (-23.08%)` | :arrow_down: |
| [app/vmalert/recording.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvcmVjb3JkaW5nLmdv) | `64.63% <0.00%> (-6.10%)` | :arrow_down: |
| [lib/storage/index\_db.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvaW5kZXhfZGIuZ28=) | `75.28% <0.00%> (-0.49%)` | :arrow_down: |
| [lib/mergeset/table.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-bGliL21lcmdlc2V0L3RhYmxlLmdv) | `67.78% <0.00%> (-0.28%)` | :arrow_down: |
| [lib/storage/partition.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvcGFydGl0aW9uLmdv) | `70.96% <0.00%> (-0.24%)` | :arrow_down: |
| [lib/storage/merge.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvbWVyZ2UuZ28=) | `81.81% <0.00%> (+1.65%)` | :arrow_up: |
| ... and [2 more](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=footer). Last update [9d431a4...874d871](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/759?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 75f67b0d7..6f9421ca7 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,8 @@ See [features available for enterprise customers](https://github.com/VictoriaMet
 * [How to import data in Prometheus exposition format](#how-to-import-data-in-prometheus-exposition-format)
 * [How to import CSV data](#how-to-import-csv-data)
 * [Prometheus querying API usage](#prometheus-querying-api-usage)
+  * [Prometheus querying API enhancements](#prometheus-querying-api-enhancements)
+* [Graphite Metrics API usage](#graphite-metrics-api-usage)
 * [How to build from sources](#how-to-build-from-sources)
   * [Development build](#development-build)
   * [Production build](#production-build)
@@ -392,9 +394,11 @@ The `/api/v1/export` endpoint should return the following response:
 
 ### Querying Graphite data
 
-Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read either via
-[Prometheus querying API](#prometheus-querying-api-usage)
-or via [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml).
+Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read via the following APIs:
+
+* [Prometheus querying API](#prometheus-querying-api-usage)
+* Metric names can be explored via [Graphite metrics API](#graphite-metrics-api-usage)
+* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml)
 
 ### How to send data from OpenTSDB-compatible agents
 
@@ -547,6 +551,9 @@ It should return something like the following:
 Extra labels may be added to all the imported metrics by passing `extra_label=name=value` query args.
 For example, `/api/v1/import/prometheus?extra_label=foo=bar` would add `{foo="bar"}` label to all the imported metrics.
 
+If timestamp is missing in `<metric> <value> <timestamp>` Prometheus exposition format line, then the current timestamp is used during data ingestion.
+It can be overriden by passing unix timestamp in *milliseconds* via `timestamp` query arg. For example, `/api/v1/import/prometheus?timestamp=1594370496905`.
+
 VictoriaMetrics accepts arbitrary number of lines in a single request to `/api/v1/import/prometheus`, i.e. it supports data streaming.
 
 VictoriaMetrics also may scrape Prometheus targets - see [these docs](#how-to-scrape-prometheus-exporters-such-as-node-exporter).
@@ -585,6 +592,21 @@ Additionally VictoriaMetrics provides the following handlers:
 * `/api/v1/labels/count` - it returns a list of `label: values_count` entries. It can be used for determining labels with the maximum number of values.
 * `/api/v1/status/active_queries` - it returns a list of currently running queries.
 
+
+### Graphite Metrics API usage
+
+VictoriaMetrics supports the following handlers from [Graphite Metrics API](https://graphite-api.readthedocs.io/en/latest/api.html#the-metrics-api):
+
+* [/metrics/find](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find)
+* [/metrics/expand](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand)
+* [/metrics/index.json](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json)
+
+VictoriaMetrics accepts the following additional query args at `/metrics/find` and `/metrics/expand`:
+  * `label` - for selecting arbitrary label values. By default `label=__name__`, i.e. metric names are selected.
+  * `delimiter` - for using different delimiters in metric name hierachy. For example, `/metrics/find?delimiter=_&query=node_*` would return all the metric name prefixes
+    that start with `node_`. By default `delimiter=.`.
+
+
 ### How to build from sources
 
 We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
diff --git a/app/vmagent/prometheusimport/request_handler.go b/app/vmagent/prometheusimport/request_handler.go
index b5ef3b833..aac41d944 100644
--- a/app/vmagent/prometheusimport/request_handler.go
+++ b/app/vmagent/prometheusimport/request_handler.go
@@ -23,9 +23,13 @@ func InsertHandler(req *http.Request) error {
 	if err != nil {
 		return err
 	}
+	defaultTimestamp, err := parserCommon.GetTimestamp(req)
+	if err != nil {
+		return err
+	}
 	return writeconcurrencylimiter.Do(func() error {
 		isGzipped := req.Header.Get("Content-Encoding") == "gzip"
-		return parser.ParseStream(req.Body, isGzipped, func(rows []parser.Row) error {
+		return parser.ParseStream(req.Body, defaultTimestamp, isGzipped, func(rows []parser.Row) error {
 			return insertRows(rows, extraLabels)
 		})
 	})
diff --git a/app/vmagent/remotewrite/remotewrite.go b/app/vmagent/remotewrite/remotewrite.go
index 2acb2d457..498093d2d 100644
--- a/app/vmagent/remotewrite/remotewrite.go
+++ b/app/vmagent/remotewrite/remotewrite.go
@@ -85,7 +85,7 @@ func Init() {
 	for i, remoteWriteURL := range *remoteWriteURLs {
 		urlLabelValue := fmt.Sprintf("secret-url-%d", i+1)
 		if *showRemoteWriteURL {
-			urlLabelValue = remoteWriteURL
+			urlLabelValue = fmt.Sprintf("%d:%s", i+1, remoteWriteURL)
 		}
 		rwctx := newRemoteWriteCtx(i, remoteWriteURL, maxInmemoryBlocks, urlLabelValue)
 		rwctxs = append(rwctxs, rwctx)
@@ -192,7 +192,7 @@ type remoteWriteCtx struct {
 
 func newRemoteWriteCtx(argIdx int, remoteWriteURL string, maxInmemoryBlocks int, urlLabelValue string) *remoteWriteCtx {
 	h := xxhash.Sum64([]byte(remoteWriteURL))
-	path := fmt.Sprintf("%s/persistent-queue/%016X", *tmpDataPath, h)
+	path := fmt.Sprintf("%s/persistent-queue/%d_%016X", *tmpDataPath, argIdx+1, h)
 	fq := persistentqueue.MustOpenFastQueue(path, remoteWriteURL, maxInmemoryBlocks, maxPendingBytesPerURL.N)
 	_ = metrics.GetOrCreateGauge(fmt.Sprintf(`vmagent_remotewrite_pending_data_bytes{path=%q, url=%q}`, path, urlLabelValue), func() float64 {
 		return float64(fq.GetPendingBytes())
diff --git a/app/vmalert/config/config.go b/app/vmalert/config/config.go
index 6c38d9e67..f027dad59 100644
--- a/app/vmalert/config/config.go
+++ b/app/vmalert/config/config.go
@@ -1,6 +1,7 @@
 package config
 
 import (
+	"crypto/md5"
 	"fmt"
 	"hash/fnv"
 	"io/ioutil"
@@ -24,11 +25,30 @@ type Group struct {
 	Interval    time.Duration `yaml:"interval,omitempty"`
 	Rules       []Rule        `yaml:"rules"`
 	Concurrency int           `yaml:"concurrency"`
+	// Checksum stores the hash of yaml definition for this group.
+	// May be used to detect any changes like rules re-ordering etc.
+	Checksum string
 
 	// Catches all undefined fields and must be empty after parsing.
 	XXX map[string]interface{} `yaml:",inline"`
 }
 
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (g *Group) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	type group Group
+	if err := unmarshal((*group)(g)); err != nil {
+		return err
+	}
+	b, err := yaml.Marshal(g)
+	if err != nil {
+		return fmt.Errorf("failed to marshal group configuration for checksum: %s", err)
+	}
+	h := md5.New()
+	h.Write(b)
+	g.Checksum = fmt.Sprintf("%x", h.Sum(nil))
+	return nil
+}
+
 // Validate check for internal Group or Rule configuration errors
 func (g *Group) Validate(validateAnnotations, validateExpressions bool) error {
 	if g.Name == "" {
@@ -101,7 +121,7 @@ func (r *Rule) Name() string {
 }
 
 // HashRule hashes significant Rule fields into
-// unique hash value
+// unique hash that supposed to define Rule uniqueness
 func HashRule(r Rule) uint64 {
 	h := fnv.New64a()
 	h.Write([]byte(r.Expr))
@@ -112,16 +132,7 @@ func HashRule(r Rule) uint64 {
 		h.Write([]byte("alerting"))
 		h.Write([]byte(r.Alert))
 	}
-	type item struct {
-		key, value string
-	}
-	var kv []item
-	for k, v := range r.Labels {
-		kv = append(kv, item{key: k, value: v})
-	}
-	sort.Slice(kv, func(i, j int) bool {
-		return kv[i].key < kv[j].key
-	})
+	kv := sortMap(r.Labels)
 	for _, i := range kv {
 		h.Write([]byte(i.key))
 		h.Write([]byte(i.value))
@@ -204,3 +215,18 @@ func checkOverflow(m map[string]interface{}, ctx string) error {
 	}
 	return nil
 }
+
+type item struct {
+	key, value string
+}
+
+func sortMap(m map[string]string) []item {
+	var kv []item
+	for k, v := range m {
+		kv = append(kv, item{key: k, value: v})
+	}
+	sort.Slice(kv, func(i, j int) bool {
+		return kv[i].key < kv[j].key
+	})
+	return kv
+}
diff --git a/app/vmalert/group.go b/app/vmalert/group.go
index e94dec112..f2a1b5bd7 100644
--- a/app/vmalert/group.go
+++ b/app/vmalert/group.go
@@ -24,6 +24,7 @@ type Group struct {
 	Rules       []Rule
 	Interval    time.Duration
 	Concurrency int
+	Checksum    string
 
 	doneCh     chan struct{}
 	finishedCh chan struct{}
@@ -53,6 +54,7 @@ func newGroup(cfg config.Group, defaultInterval time.Duration, labels map[string
 		File:        cfg.File,
 		Interval:    cfg.Interval,
 		Concurrency: cfg.Concurrency,
+		Checksum:    cfg.Checksum,
 		doneCh:      make(chan struct{}),
 		finishedCh:  make(chan struct{}),
 		updateCh:    make(chan *Group),
@@ -156,6 +158,7 @@ func (g *Group) updateWith(newGroup *Group) error {
 		newRules = append(newRules, nr)
 	}
 	g.Concurrency = newGroup.Concurrency
+	g.Checksum = newGroup.Checksum
 	g.Rules = newRules
 	return nil
 }
diff --git a/app/vmalert/manager.go b/app/vmalert/manager.go
index 2c749b830..3abdd1340 100644
--- a/app/vmalert/manager.go
+++ b/app/vmalert/manager.go
@@ -93,31 +93,53 @@ func (m *manager) update(ctx context.Context, path []string, validateTpl, valida
 		groupsRegistry[ng.ID()] = ng
 	}
 
+	type updateItem struct {
+		old *Group
+		new *Group
+	}
+	var toUpdate []updateItem
+
 	m.groupsMu.Lock()
 	for _, og := range m.groups {
 		ng, ok := groupsRegistry[og.ID()]
 		if !ok {
-			// old group is not present in new list
-			// and must be stopped and deleted
+			// old group is not present in new list,
+			// so must be stopped and deleted
 			og.close()
 			delete(m.groups, og.ID())
 			og = nil
 			continue
 		}
-		og.updateCh <- ng
 		delete(groupsRegistry, ng.ID())
+		if og.Checksum != ng.Checksum {
+			toUpdate = append(toUpdate, updateItem{old: og, new: ng})
+		}
 	}
-
 	for _, ng := range groupsRegistry {
 		m.startGroup(ctx, ng, restore)
 	}
 	m.groupsMu.Unlock()
+
+	if len(toUpdate) > 0 {
+		var wg sync.WaitGroup
+		for _, item := range toUpdate {
+			wg.Add(1)
+			go func(old *Group, new *Group) {
+				old.updateCh <- new
+				wg.Done()
+			}(item.old, item.new)
+		}
+		wg.Wait()
+	}
 	return nil
 }
 
 func (g *Group) toAPI() APIGroup {
+	g.mu.RLock()
+	defer g.mu.RUnlock()
+
 	ag := APIGroup{
-		// encode as strings to avoid rounding
+		// encode as string to avoid rounding
 		ID:          fmt.Sprintf("%d", g.ID()),
 		Name:        g.Name,
 		File:        g.File,
diff --git a/app/vminsert/prometheusimport/request_handler.go b/app/vminsert/prometheusimport/request_handler.go
index fa7d1068a..c499600cd 100644
--- a/app/vminsert/prometheusimport/request_handler.go
+++ b/app/vminsert/prometheusimport/request_handler.go
@@ -23,9 +23,13 @@ func InsertHandler(req *http.Request) error {
 	if err != nil {
 		return err
 	}
+	defaultTimestamp, err := parserCommon.GetTimestamp(req)
+	if err != nil {
+		return err
+	}
 	return writeconcurrencylimiter.Do(func() error {
 		isGzipped := req.Header.Get("Content-Encoding") == "gzip"
-		return parser.ParseStream(req.Body, isGzipped, func(rows []parser.Row) error {
+		return parser.ParseStream(req.Body, defaultTimestamp, isGzipped, func(rows []parser.Row) error {
 			return insertRows(rows, extraLabels)
 		})
 	})
diff --git a/app/vmselect/graphite/graphite.go b/app/vmselect/graphite/graphite.go
new file mode 100644
index 000000000..25c63f6eb
--- /dev/null
+++ b/app/vmselect/graphite/graphite.go
@@ -0,0 +1,383 @@
+package graphite
+
+import (
+	"fmt"
+	"net/http"
+	"regexp"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
+	"github.com/VictoriaMetrics/metrics"
+)
+
+// MetricsFindHandler implements /metrics/find handler.
+//
+// See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find
+func MetricsFindHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
+	if err := r.ParseForm(); err != nil {
+		return fmt.Errorf("cannot parse form values: %w", err)
+	}
+	format := r.FormValue("format")
+	if format == "" {
+		format = "treejson"
+	}
+	switch format {
+	case "treejson", "completer":
+	default:
+		return fmt.Errorf(`unexpected "format" query arg: %q; expecting "treejson" or "completer"`, format)
+	}
+	query := r.FormValue("query")
+	if len(query) == 0 {
+		return fmt.Errorf("expecting non-empty `query` arg")
+	}
+	delimiter := r.FormValue("delimiter")
+	if delimiter == "" {
+		delimiter = "."
+	}
+	if len(delimiter) > 1 {
+		return fmt.Errorf("`delimiter` query arg must contain only a single char")
+	}
+	if searchutils.GetBool(r, "automatic_variants") {
+		// See https://github.com/graphite-project/graphite-web/blob/bb9feb0e6815faa73f538af6ed35adea0fb273fd/webapp/graphite/metrics/views.py#L152
+		query = addAutomaticVariants(query, delimiter)
+	}
+	if format == "completer" {
+		// See https://github.com/graphite-project/graphite-web/blob/bb9feb0e6815faa73f538af6ed35adea0fb273fd/webapp/graphite/metrics/views.py#L148
+		query = strings.ReplaceAll(query, "..", ".*")
+		if !strings.HasSuffix(query, "*") {
+			query += "*"
+		}
+	}
+	leavesOnly := searchutils.GetBool(r, "leavesOnly")
+	wildcards := searchutils.GetBool(r, "wildcards")
+	label := r.FormValue("label")
+	if label == "__name__" {
+		label = ""
+	}
+	jsonp := r.FormValue("jsonp")
+	from, err := searchutils.GetTime(r, "from", 0)
+	if err != nil {
+		return err
+	}
+	ct := startTime.UnixNano() / 1e6
+	until, err := searchutils.GetTime(r, "until", ct)
+	if err != nil {
+		return err
+	}
+	tr := storage.TimeRange{
+		MinTimestamp: from,
+		MaxTimestamp: until,
+	}
+	paths, err := metricsFind(tr, label, query, delimiter[0], deadline)
+	if err != nil {
+		return err
+	}
+	if leavesOnly {
+		paths = filterLeaves(paths, delimiter)
+	}
+	sortPaths(paths, delimiter)
+	contentType := "application/json"
+	if jsonp != "" {
+		contentType = "text/javascript"
+	}
+	w.Header().Set("Content-Type", contentType)
+	WriteMetricsFindResponse(w, paths, delimiter, format, wildcards, jsonp)
+	metricsFindDuration.UpdateDuration(startTime)
+	return nil
+}
+
+// MetricsExpandHandler implements /metrics/expand handler.
+//
+// See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand
+func MetricsExpandHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
+	if err := r.ParseForm(); err != nil {
+		return fmt.Errorf("cannot parse form values: %w", err)
+	}
+	queries := r.Form["query"]
+	if len(queries) == 0 {
+		return fmt.Errorf("missing `query` arg")
+	}
+	groupByExpr := searchutils.GetBool(r, "groupByExpr")
+	leavesOnly := searchutils.GetBool(r, "leavesOnly")
+	label := r.FormValue("label")
+	if label == "__name__" {
+		label = ""
+	}
+	delimiter := r.FormValue("delimiter")
+	if delimiter == "" {
+		delimiter = "."
+	}
+	if len(delimiter) > 1 {
+		return fmt.Errorf("`delimiter` query arg must contain only a single char")
+	}
+	jsonp := r.FormValue("jsonp")
+	from, err := searchutils.GetTime(r, "from", 0)
+	if err != nil {
+		return err
+	}
+	ct := startTime.UnixNano() / 1e6
+	until, err := searchutils.GetTime(r, "until", ct)
+	if err != nil {
+		return err
+	}
+	tr := storage.TimeRange{
+		MinTimestamp: from,
+		MaxTimestamp: until,
+	}
+	m := make(map[string][]string, len(queries))
+	for _, query := range queries {
+		paths, err := metricsFind(tr, label, query, delimiter[0], deadline)
+		if err != nil {
+			return err
+		}
+		if leavesOnly {
+			paths = filterLeaves(paths, delimiter)
+		}
+		m[query] = paths
+	}
+	contentType := "application/json"
+	if jsonp != "" {
+		contentType = "text/javascript"
+	}
+	w.Header().Set("Content-Type", contentType)
+	if groupByExpr {
+		for _, paths := range m {
+			sortPaths(paths, delimiter)
+		}
+		WriteMetricsExpandResponseByQuery(w, m, jsonp)
+		return nil
+	}
+	paths := m[queries[0]]
+	if len(m) > 1 {
+		pathsSet := make(map[string]struct{})
+		for _, paths := range m {
+			for _, path := range paths {
+				pathsSet[path] = struct{}{}
+			}
+		}
+		paths = make([]string, 0, len(pathsSet))
+		for path := range pathsSet {
+			paths = append(paths, path)
+		}
+	}
+	sortPaths(paths, delimiter)
+	WriteMetricsExpandResponseFlat(w, paths, jsonp)
+	metricsExpandDuration.UpdateDuration(startTime)
+	return nil
+}
+
+// MetricsIndexHandler implements /metrics/index.json handler.
+//
+// See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json
+func MetricsIndexHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
+	if err := r.ParseForm(); err != nil {
+		return fmt.Errorf("cannot parse form values: %w", err)
+	}
+	jsonp := r.FormValue("jsonp")
+	metricNames, err := netstorage.GetLabelValues("__name__", deadline)
+	if err != nil {
+		return fmt.Errorf(`cannot obtain metric names: %w`, err)
+	}
+	contentType := "application/json"
+	if jsonp != "" {
+		contentType = "text/javascript"
+	}
+	w.Header().Set("Content-Type", contentType)
+	WriteMetricsIndexResponse(w, metricNames, jsonp)
+	metricsIndexDuration.UpdateDuration(startTime)
+	return nil
+}
+
+// metricsFind searches for label values that match the given query.
+func metricsFind(tr storage.TimeRange, label, query string, delimiter byte, deadline searchutils.Deadline) ([]string, error) {
+	expandTail := strings.HasSuffix(query, "*")
+	for strings.HasSuffix(query, "*") {
+		query = query[:len(query)-1]
+	}
+	var results []string
+	n := strings.IndexAny(query, "*{[")
+	if n < 0 {
+		suffixes, err := netstorage.GetTagValueSuffixes(tr, label, query, delimiter, deadline)
+		if err != nil {
+			return nil, err
+		}
+		if expandTail {
+			for _, suffix := range suffixes {
+				results = append(results, query+suffix)
+			}
+		} else if isFullMatch(query, suffixes, delimiter) {
+			results = append(results, query)
+		}
+		return results, nil
+	}
+	subquery := query[:n] + "*"
+	paths, err := metricsFind(tr, label, subquery, delimiter, deadline)
+	if err != nil {
+		return nil, err
+	}
+	tail := ""
+	suffix := query[n:]
+	if m := strings.IndexByte(suffix, delimiter); m >= 0 {
+		tail = suffix[m+1:]
+		suffix = suffix[:m+1]
+	}
+	q := query[:n] + suffix
+	re, err := getRegexpForQuery(q, delimiter)
+	if err != nil {
+		return nil, fmt.Errorf("cannot convert query %q to regexp: %w", q, err)
+	}
+	if expandTail {
+		tail += "*"
+	}
+	for _, path := range paths {
+		if !re.MatchString(path) {
+			continue
+		}
+		subquery := path + tail
+		tmp, err := metricsFind(tr, label, subquery, delimiter, deadline)
+		if err != nil {
+			return nil, err
+		}
+		results = append(results, tmp...)
+	}
+	return results, nil
+}
+
+var (
+	metricsFindDuration   = metrics.NewSummary(`vm_request_duration_seconds{path="/metrics/find"}`)
+	metricsExpandDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/metrics/expand"}`)
+	metricsIndexDuration  = metrics.NewSummary(`vm_request_duration_seconds{path="/metrics/index.json"}`)
+)
+
+func isFullMatch(tagValuePrefix string, suffixes []string, delimiter byte) bool {
+	if len(suffixes) == 0 {
+		return false
+	}
+	if strings.LastIndexByte(tagValuePrefix, delimiter) == len(tagValuePrefix)-1 {
+		return true
+	}
+	for _, suffix := range suffixes {
+		if suffix == "" {
+			return true
+		}
+	}
+	return false
+}
+
+func addAutomaticVariants(query, delimiter string) string {
+	// See https://github.com/graphite-project/graphite-web/blob/bb9feb0e6815faa73f538af6ed35adea0fb273fd/webapp/graphite/metrics/views.py#L152
+	parts := strings.Split(query, delimiter)
+	for i, part := range parts {
+		if strings.Contains(part, ",") && !strings.Contains(part, "{") {
+			parts[i] = "{" + part + "}"
+		}
+	}
+	return strings.Join(parts, delimiter)
+}
+
+func filterLeaves(paths []string, delimiter string) []string {
+	leaves := paths[:0]
+	for _, path := range paths {
+		if !strings.HasSuffix(path, delimiter) {
+			leaves = append(leaves, path)
+		}
+	}
+	return leaves
+}
+
+func sortPaths(paths []string, delimiter string) {
+	sort.Slice(paths, func(i, j int) bool {
+		a, b := paths[i], paths[j]
+		isNodeA := strings.HasSuffix(a, delimiter)
+		isNodeB := strings.HasSuffix(b, delimiter)
+		if isNodeA == isNodeB {
+			return a < b
+		}
+		return isNodeA
+	})
+}
+
+func getRegexpForQuery(query string, delimiter byte) (*regexp.Regexp, error) {
+	regexpCacheLock.Lock()
+	defer regexpCacheLock.Unlock()
+
+	k := regexpCacheKey{
+		query:     query,
+		delimiter: delimiter,
+	}
+	if re := regexpCache[k]; re != nil {
+		return re.re, re.err
+	}
+	a := make([]string, 0, len(query))
+	tillNextDelimiter := "[^" + regexp.QuoteMeta(string([]byte{delimiter})) + "]*"
+	for i := 0; i < len(query); i++ {
+		switch query[i] {
+		case '*':
+			a = append(a, tillNextDelimiter)
+		case '{':
+			tmp := query[i+1:]
+			if n := strings.IndexByte(tmp, '}'); n < 0 {
+				a = append(a, regexp.QuoteMeta(query[i:]))
+				i = len(query)
+			} else {
+				a = append(a, "(?:")
+				opts := strings.Split(tmp[:n], ",")
+				for j, opt := range opts {
+					opts[j] = regexp.QuoteMeta(opt)
+				}
+				a = append(a, strings.Join(opts, "|"))
+				a = append(a, ")")
+				i += n + 1
+			}
+		case '[':
+			tmp := query[i:]
+			if n := strings.IndexByte(tmp, ']'); n < 0 {
+				a = append(a, regexp.QuoteMeta(query[i:]))
+				i = len(query)
+			} else {
+				a = append(a, tmp[:n+1])
+				i += n
+			}
+		default:
+			a = append(a, regexp.QuoteMeta(query[i:i+1]))
+		}
+	}
+	s := strings.Join(a, "")
+	re, err := regexp.Compile(s)
+	regexpCache[k] = &regexpCacheEntry{
+		re:  re,
+		err: err,
+	}
+	if len(regexpCache) >= maxRegexpCacheSize {
+		for k := range regexpCache {
+			if len(regexpCache) < maxRegexpCacheSize {
+				break
+			}
+			delete(regexpCache, k)
+		}
+	}
+	return re, err
+}
+
+type regexpCacheEntry struct {
+	re  *regexp.Regexp
+	err error
+}
+
+type regexpCacheKey struct {
+	query     string
+	delimiter byte
+}
+
+var regexpCache = make(map[regexpCacheKey]*regexpCacheEntry)
+var regexpCacheLock sync.Mutex
+
+const maxRegexpCacheSize = 10000
diff --git a/app/vmselect/graphite/metrics_expand_response.qtpl b/app/vmselect/graphite/metrics_expand_response.qtpl
new file mode 100644
index 000000000..f33bd9a6a
--- /dev/null
+++ b/app/vmselect/graphite/metrics_expand_response.qtpl
@@ -0,0 +1,38 @@
+{% stripspace %}
+
+MetricsExpandResponseByQuery generates response for /metrics/expand?groupByExpr=1 .
+See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand
+{% func MetricsExpandResponseByQuery(m map[string][]string, jsonp string) %}
+	{% if jsonp != "" %}{%s= jsonp %}({% endif %}
+	{
+		"results":{
+			{% code i := 0 %}
+			{% for query, paths := range m %}
+				{%q= query %}:{%= metricPaths(paths) %}
+				{% code i++ %}
+				{% if i < len(m) %},{% endif %}
+			{% endfor %}
+		}
+	}
+	{% if jsonp != "" %}){% endif %}
+{% endfunc %}
+
+
+MetricsExpandResponseFlat generates response for /metrics/expand?groupByExpr=0 .
+See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand
+{% func MetricsExpandResponseFlat(paths []string, jsonp string) %}
+	{% if jsonp != "" %}{%s= jsonp %}({% endif %}
+	{%= metricPaths(paths) %}
+	{% if jsonp != "" %}){% endif %}
+{% endfunc %}
+
+{% func metricPaths(paths []string) %}
+	[
+		{% for i, path := range paths %}
+			{%q= path %}
+			{% if i+1 < len(paths) %},{% endif %}
+		{% endfor %}
+	]
+{% endfunc %}
+
+{% endstripspace %}
diff --git a/app/vmselect/graphite/metrics_expand_response.qtpl.go b/app/vmselect/graphite/metrics_expand_response.qtpl.go
new file mode 100644
index 000000000..6101f6a03
--- /dev/null
+++ b/app/vmselect/graphite/metrics_expand_response.qtpl.go
@@ -0,0 +1,187 @@
+// Code generated by qtc from "metrics_expand_response.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+// MetricsExpandResponseByQuery generates response for /metrics/expand?groupByExpr=1 .See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:5
+package graphite
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:5
+import (
+	qtio422016 "io"
+
+	qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:5
+var (
+	_ = qtio422016.Copy
+	_ = qt422016.AcquireByteBuffer
+)
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:5
+func StreamMetricsExpandResponseByQuery(qw422016 *qt422016.Writer, m map[string][]string, jsonp string) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:6
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:6
+		qw422016.N().S(jsonp)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:6
+		qw422016.N().S(`(`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:6
+	}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:6
+	qw422016.N().S(`{"results":{`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:9
+	i := 0
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:10
+	for query, paths := range m {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:11
+		qw422016.N().Q(query)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:11
+		qw422016.N().S(`:`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:11
+		streammetricPaths(qw422016, paths)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:12
+		i++
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:13
+		if i < len(m) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:13
+			qw422016.N().S(`,`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:13
+		}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:14
+	}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:14
+	qw422016.N().S(`}}`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:17
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:17
+		qw422016.N().S(`)`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:17
+	}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+func WriteMetricsExpandResponseByQuery(qq422016 qtio422016.Writer, m map[string][]string, jsonp string) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	StreamMetricsExpandResponseByQuery(qw422016, m, jsonp)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+func MetricsExpandResponseByQuery(m map[string][]string, jsonp string) string {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	WriteMetricsExpandResponseByQuery(qb422016, m, jsonp)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+	return qs422016
+//line app/vmselect/graphite/metrics_expand_response.qtpl:18
+}
+
+// MetricsExpandResponseFlat generates response for /metrics/expand?groupByExpr=0 .See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:23
+func StreamMetricsExpandResponseFlat(qw422016 *qt422016.Writer, paths []string, jsonp string) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:24
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:24
+		qw422016.N().S(jsonp)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:24
+		qw422016.N().S(`(`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:24
+	}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:25
+	streammetricPaths(qw422016, paths)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:26
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:26
+		qw422016.N().S(`)`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:26
+	}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+func WriteMetricsExpandResponseFlat(qq422016 qtio422016.Writer, paths []string, jsonp string) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	StreamMetricsExpandResponseFlat(qw422016, paths, jsonp)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+func MetricsExpandResponseFlat(paths []string, jsonp string) string {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	WriteMetricsExpandResponseFlat(qb422016, paths, jsonp)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+	return qs422016
+//line app/vmselect/graphite/metrics_expand_response.qtpl:27
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:29
+func streammetricPaths(qw422016 *qt422016.Writer, paths []string) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:29
+	qw422016.N().S(`[`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:31
+	for i, path := range paths {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:32
+		qw422016.N().Q(path)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:33
+		if i+1 < len(paths) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:33
+			qw422016.N().S(`,`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:33
+		}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:34
+	}
+//line app/vmselect/graphite/metrics_expand_response.qtpl:34
+	qw422016.N().S(`]`)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+func writemetricPaths(qq422016 qtio422016.Writer, paths []string) {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	streammetricPaths(qw422016, paths)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+}
+
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+func metricPaths(paths []string) string {
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	writemetricPaths(qb422016, paths)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+	return qs422016
+//line app/vmselect/graphite/metrics_expand_response.qtpl:36
+}
diff --git a/app/vmselect/graphite/metrics_find_response.qtpl b/app/vmselect/graphite/metrics_find_response.qtpl
new file mode 100644
index 000000000..d073bc7b6
--- /dev/null
+++ b/app/vmselect/graphite/metrics_find_response.qtpl
@@ -0,0 +1,110 @@
+{% import (
+	"strings"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+) %}
+
+{% stripspace %}
+
+MetricsFindResponse generates response for /metrics/find .
+See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find
+{% func MetricsFindResponse(paths []string, delimiter, format string, addWildcards bool, jsonp string) %}
+	{% if jsonp != "" %}{%s= jsonp %}({% endif %}
+	{% switch format %}
+	{% case "completer" %}
+		{%= metricsFindResponseCompleter(paths, delimiter, addWildcards) %}
+	{% case "treejson" %}
+		{%= metricsFindResponseTreeJSON(paths, delimiter, addWildcards) %}
+	{% default %}
+		{% code logger.Panicf("BUG: unexpected format=%q", format) %}
+	{% endswitch %}
+	{% if jsonp != "" %}){% endif %}
+{% endfunc %}
+
+{% func metricsFindResponseCompleter(paths []string, delimiter string, addWildcards bool) %}
+{
+	"metrics":[
+		{% for i, path := range paths %}
+		{
+			"path": {%q= path %},
+			"name": {%= metricPathName(path, delimiter) %},
+			"is_leaf": {% if strings.HasSuffix(path, delimiter) %}0{% else %}1{% endif %}
+		}
+		{% if i+1 < len(paths) %},{% endif %}
+		{% endfor %}
+		{% if addWildcards && len(paths) > 1 %}
+		,{
+			"name": "*"
+		}
+		{% endif %}
+	]
+}
+{% endfunc %}
+
+{% func metricsFindResponseTreeJSON(paths []string, delimiter string, addWildcards bool) %}
+[
+	{% for i, path := range paths %}
+	{
+		{% code
+			allowChildren := "0"
+			isLeaf := "1"
+			if strings.HasSuffix(path, delimiter) {
+				allowChildren = "1"
+				isLeaf = "0"
+			}
+		%}
+		"id": {%q= path %},
+		"text": {%= metricPathName(path, delimiter) %},
+		"allowChildren": {%s= allowChildren %},
+		"expandable": {%s= allowChildren %},
+		"leaf": {%s= isLeaf %}
+	}
+	{% if i+1 < len(paths) %},{% endif %}
+	{% endfor %}
+	{% if addWildcards && len(paths) > 1 %}
+	,{
+		{% code
+			path := paths[0]
+			for strings.HasSuffix(path, delimiter) {
+				path = path[:len(path)-1]
+			}
+			id := ""
+			if n := strings.LastIndexByte(path, delimiter[0]); n >= 0 {
+				id = path[:n+1]
+			}
+			id += "*"
+
+			allowChildren := "0"
+			isLeaf := "1"
+			for _, path := range paths {
+				if strings.HasSuffix(path, delimiter) {
+					allowChildren = "1"
+					isLeaf = "0"
+					break
+				}
+			}
+		%}
+		"id": {%q= id %},
+		"text": "*",
+		"allowChildren": {%s= allowChildren %},
+		"expandable": {%s= allowChildren %},
+		"leaf": {%s= isLeaf %}
+	}
+	{% endif %}
+]
+{% endfunc %}
+
+{% func metricPathName(path, delimiter string) %}
+	{% code
+		name := path
+		for strings.HasSuffix(name, delimiter) {
+			name = name[:len(name)-1]
+		}
+		if n := strings.LastIndexByte(name, delimiter[0]); n >= 0 {
+			name = name[n+1:]
+		}
+	%}
+	{%q= name %}
+{% endfunc %}
+
+{% endstripspace %}
diff --git a/app/vmselect/graphite/metrics_find_response.qtpl.go b/app/vmselect/graphite/metrics_find_response.qtpl.go
new file mode 100644
index 000000000..00b8c2555
--- /dev/null
+++ b/app/vmselect/graphite/metrics_find_response.qtpl.go
@@ -0,0 +1,326 @@
+// Code generated by qtc from "metrics_find_response.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:1
+package graphite
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:1
+import (
+	"strings"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
+)
+
+// MetricsFindResponse generates response for /metrics/find .See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:11
+import (
+	qtio422016 "io"
+
+	qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:11
+var (
+	_ = qtio422016.Copy
+	_ = qt422016.AcquireByteBuffer
+)
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:11
+func StreamMetricsFindResponse(qw422016 *qt422016.Writer, paths []string, delimiter, format string, addWildcards bool, jsonp string) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:12
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_find_response.qtpl:12
+		qw422016.N().S(jsonp)
+//line app/vmselect/graphite/metrics_find_response.qtpl:12
+		qw422016.N().S(`(`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:12
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:13
+	switch format {
+//line app/vmselect/graphite/metrics_find_response.qtpl:14
+	case "completer":
+//line app/vmselect/graphite/metrics_find_response.qtpl:15
+		streammetricsFindResponseCompleter(qw422016, paths, delimiter, addWildcards)
+//line app/vmselect/graphite/metrics_find_response.qtpl:16
+	case "treejson":
+//line app/vmselect/graphite/metrics_find_response.qtpl:17
+		streammetricsFindResponseTreeJSON(qw422016, paths, delimiter, addWildcards)
+//line app/vmselect/graphite/metrics_find_response.qtpl:18
+	default:
+//line app/vmselect/graphite/metrics_find_response.qtpl:19
+		logger.Panicf("BUG: unexpected format=%q", format)
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:20
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:21
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_find_response.qtpl:21
+		qw422016.N().S(`)`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:21
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+func WriteMetricsFindResponse(qq422016 qtio422016.Writer, paths []string, delimiter, format string, addWildcards bool, jsonp string) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	StreamMetricsFindResponse(qw422016, paths, delimiter, format, addWildcards, jsonp)
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+func MetricsFindResponse(paths []string, delimiter, format string, addWildcards bool, jsonp string) string {
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	WriteMetricsFindResponse(qb422016, paths, delimiter, format, addWildcards, jsonp)
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+	return qs422016
+//line app/vmselect/graphite/metrics_find_response.qtpl:22
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:24
+func streammetricsFindResponseCompleter(qw422016 *qt422016.Writer, paths []string, delimiter string, addWildcards bool) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:24
+	qw422016.N().S(`{"metrics":[`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:27
+	for i, path := range paths {
+//line app/vmselect/graphite/metrics_find_response.qtpl:27
+		qw422016.N().S(`{"path":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:29
+		qw422016.N().Q(path)
+//line app/vmselect/graphite/metrics_find_response.qtpl:29
+		qw422016.N().S(`,"name":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:30
+		streammetricPathName(qw422016, path, delimiter)
+//line app/vmselect/graphite/metrics_find_response.qtpl:30
+		qw422016.N().S(`,"is_leaf":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:31
+		if strings.HasSuffix(path, delimiter) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:31
+			qw422016.N().S(`0`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:31
+		} else {
+//line app/vmselect/graphite/metrics_find_response.qtpl:31
+			qw422016.N().S(`1`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:31
+		}
+//line app/vmselect/graphite/metrics_find_response.qtpl:31
+		qw422016.N().S(`}`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:33
+		if i+1 < len(paths) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:33
+			qw422016.N().S(`,`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:33
+		}
+//line app/vmselect/graphite/metrics_find_response.qtpl:34
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:35
+	if addWildcards && len(paths) > 1 {
+//line app/vmselect/graphite/metrics_find_response.qtpl:35
+		qw422016.N().S(`,{"name": "*"}`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:39
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:39
+	qw422016.N().S(`]}`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+func writemetricsFindResponseCompleter(qq422016 qtio422016.Writer, paths []string, delimiter string, addWildcards bool) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	streammetricsFindResponseCompleter(qw422016, paths, delimiter, addWildcards)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+func metricsFindResponseCompleter(paths []string, delimiter string, addWildcards bool) string {
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	writemetricsFindResponseCompleter(qb422016, paths, delimiter, addWildcards)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+	return qs422016
+//line app/vmselect/graphite/metrics_find_response.qtpl:42
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:44
+func streammetricsFindResponseTreeJSON(qw422016 *qt422016.Writer, paths []string, delimiter string, addWildcards bool) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:44
+	qw422016.N().S(`[`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:46
+	for i, path := range paths {
+//line app/vmselect/graphite/metrics_find_response.qtpl:46
+		qw422016.N().S(`{`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:49
+		allowChildren := "0"
+		isLeaf := "1"
+		if strings.HasSuffix(path, delimiter) {
+			allowChildren = "1"
+			isLeaf = "0"
+		}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:55
+		qw422016.N().S(`"id":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:56
+		qw422016.N().Q(path)
+//line app/vmselect/graphite/metrics_find_response.qtpl:56
+		qw422016.N().S(`,"text":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:57
+		streammetricPathName(qw422016, path, delimiter)
+//line app/vmselect/graphite/metrics_find_response.qtpl:57
+		qw422016.N().S(`,"allowChildren":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:58
+		qw422016.N().S(allowChildren)
+//line app/vmselect/graphite/metrics_find_response.qtpl:58
+		qw422016.N().S(`,"expandable":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:59
+		qw422016.N().S(allowChildren)
+//line app/vmselect/graphite/metrics_find_response.qtpl:59
+		qw422016.N().S(`,"leaf":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:60
+		qw422016.N().S(isLeaf)
+//line app/vmselect/graphite/metrics_find_response.qtpl:60
+		qw422016.N().S(`}`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:62
+		if i+1 < len(paths) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:62
+			qw422016.N().S(`,`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:62
+		}
+//line app/vmselect/graphite/metrics_find_response.qtpl:63
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:64
+	if addWildcards && len(paths) > 1 {
+//line app/vmselect/graphite/metrics_find_response.qtpl:64
+		qw422016.N().S(`,{`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:67
+		path := paths[0]
+		for strings.HasSuffix(path, delimiter) {
+			path = path[:len(path)-1]
+		}
+		id := ""
+		if n := strings.LastIndexByte(path, delimiter[0]); n >= 0 {
+			id = path[:n+1]
+		}
+		id += "*"
+
+		allowChildren := "0"
+		isLeaf := "1"
+		for _, path := range paths {
+			if strings.HasSuffix(path, delimiter) {
+				allowChildren = "1"
+				isLeaf = "0"
+				break
+			}
+		}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:86
+		qw422016.N().S(`"id":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:87
+		qw422016.N().Q(id)
+//line app/vmselect/graphite/metrics_find_response.qtpl:87
+		qw422016.N().S(`,"text": "*","allowChildren":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:89
+		qw422016.N().S(allowChildren)
+//line app/vmselect/graphite/metrics_find_response.qtpl:89
+		qw422016.N().S(`,"expandable":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:90
+		qw422016.N().S(allowChildren)
+//line app/vmselect/graphite/metrics_find_response.qtpl:90
+		qw422016.N().S(`,"leaf":`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:91
+		qw422016.N().S(isLeaf)
+//line app/vmselect/graphite/metrics_find_response.qtpl:91
+		qw422016.N().S(`}`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:93
+	}
+//line app/vmselect/graphite/metrics_find_response.qtpl:93
+	qw422016.N().S(`]`)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+func writemetricsFindResponseTreeJSON(qq422016 qtio422016.Writer, paths []string, delimiter string, addWildcards bool) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	streammetricsFindResponseTreeJSON(qw422016, paths, delimiter, addWildcards)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+func metricsFindResponseTreeJSON(paths []string, delimiter string, addWildcards bool) string {
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	writemetricsFindResponseTreeJSON(qb422016, paths, delimiter, addWildcards)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+	return qs422016
+//line app/vmselect/graphite/metrics_find_response.qtpl:95
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:97
+func streammetricPathName(qw422016 *qt422016.Writer, path, delimiter string) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:99
+	name := path
+	for strings.HasSuffix(name, delimiter) {
+		name = name[:len(name)-1]
+	}
+	if n := strings.LastIndexByte(name, delimiter[0]); n >= 0 {
+		name = name[n+1:]
+	}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:107
+	qw422016.N().Q(name)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+func writemetricPathName(qq422016 qtio422016.Writer, path, delimiter string) {
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	streammetricPathName(qw422016, path, delimiter)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+}
+
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+func metricPathName(path, delimiter string) string {
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	writemetricPathName(qb422016, path, delimiter)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+	return qs422016
+//line app/vmselect/graphite/metrics_find_response.qtpl:108
+}
diff --git a/app/vmselect/graphite/metrics_index_response.qtpl b/app/vmselect/graphite/metrics_index_response.qtpl
new file mode 100644
index 000000000..4e832ab86
--- /dev/null
+++ b/app/vmselect/graphite/metrics_index_response.qtpl
@@ -0,0 +1,11 @@
+{% stripspace %}
+
+MetricsIndexResponse generates response for /metrics/index.json .
+See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json
+{% func MetricsIndexResponse(metricNames []string, jsonp string) %}
+	{% if jsonp != "" %}{%s= jsonp %}({% endif %}
+	{%= metricPaths(metricNames) %}
+	{% if jsonp != "" %}){% endif %}
+{% endfunc %}
+
+{% endstripspace %}
diff --git a/app/vmselect/graphite/metrics_index_response.qtpl.go b/app/vmselect/graphite/metrics_index_response.qtpl.go
new file mode 100644
index 000000000..6e29799ba
--- /dev/null
+++ b/app/vmselect/graphite/metrics_index_response.qtpl.go
@@ -0,0 +1,67 @@
+// Code generated by qtc from "metrics_index_response.qtpl". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+// MetricsIndexResponse generates response for /metrics/index.json .See https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json
+
+//line app/vmselect/graphite/metrics_index_response.qtpl:5
+package graphite
+
+//line app/vmselect/graphite/metrics_index_response.qtpl:5
+import (
+	qtio422016 "io"
+
+	qt422016 "github.com/valyala/quicktemplate"
+)
+
+//line app/vmselect/graphite/metrics_index_response.qtpl:5
+var (
+	_ = qtio422016.Copy
+	_ = qt422016.AcquireByteBuffer
+)
+
+//line app/vmselect/graphite/metrics_index_response.qtpl:5
+func StreamMetricsIndexResponse(qw422016 *qt422016.Writer, metricNames []string, jsonp string) {
+//line app/vmselect/graphite/metrics_index_response.qtpl:6
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_index_response.qtpl:6
+		qw422016.N().S(jsonp)
+//line app/vmselect/graphite/metrics_index_response.qtpl:6
+		qw422016.N().S(`(`)
+//line app/vmselect/graphite/metrics_index_response.qtpl:6
+	}
+//line app/vmselect/graphite/metrics_index_response.qtpl:7
+	streammetricPaths(qw422016, metricNames)
+//line app/vmselect/graphite/metrics_index_response.qtpl:8
+	if jsonp != "" {
+//line app/vmselect/graphite/metrics_index_response.qtpl:8
+		qw422016.N().S(`)`)
+//line app/vmselect/graphite/metrics_index_response.qtpl:8
+	}
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+}
+
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+func WriteMetricsIndexResponse(qq422016 qtio422016.Writer, metricNames []string, jsonp string) {
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	qw422016 := qt422016.AcquireWriter(qq422016)
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	StreamMetricsIndexResponse(qw422016, metricNames, jsonp)
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	qt422016.ReleaseWriter(qw422016)
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+}
+
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+func MetricsIndexResponse(metricNames []string, jsonp string) string {
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	qb422016 := qt422016.AcquireByteBuffer()
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	WriteMetricsIndexResponse(qb422016, metricNames, jsonp)
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	qs422016 := string(qb422016.B)
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	qt422016.ReleaseByteBuffer(qb422016)
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+	return qs422016
+//line app/vmselect/graphite/metrics_index_response.qtpl:9
+}
diff --git a/app/vmselect/main.go b/app/vmselect/main.go
index baca764e2..1c6cb940a 100644
--- a/app/vmselect/main.go
+++ b/app/vmselect/main.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
@@ -203,6 +204,33 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
 			return true
 		}
 		return true
+	case "/metrics/find", "/metrics/find/":
+		graphiteMetricsFindRequests.Inc()
+		httpserver.EnableCORS(w, r)
+		if err := graphite.MetricsFindHandler(startTime, w, r); err != nil {
+			graphiteMetricsFindErrors.Inc()
+			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
+			return true
+		}
+		return true
+	case "/metrics/expand", "/metrics/expand/":
+		graphiteMetricsExpandRequests.Inc()
+		httpserver.EnableCORS(w, r)
+		if err := graphite.MetricsExpandHandler(startTime, w, r); err != nil {
+			graphiteMetricsExpandErrors.Inc()
+			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
+			return true
+		}
+		return true
+	case "/metrics/index.json", "/metrics/index.json/":
+		graphiteMetricsIndexRequests.Inc()
+		httpserver.EnableCORS(w, r)
+		if err := graphite.MetricsIndexHandler(startTime, w, r); err != nil {
+			graphiteMetricsIndexErrors.Inc()
+			httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
+			return true
+		}
+		return true
 	case "/api/v1/rules":
 		// Return dumb placeholder
 		rulesRequests.Inc()
@@ -289,6 +317,15 @@ var (
 	federateRequests = metrics.NewCounter(`vm_http_requests_total{path="/federate"}`)
 	federateErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/federate"}`)
 
+	graphiteMetricsFindRequests = metrics.NewCounter(`vm_http_requests_total{path="/metrics/find"}`)
+	graphiteMetricsFindErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/metrics/find"}`)
+
+	graphiteMetricsExpandRequests = metrics.NewCounter(`vm_http_requests_total{path="/metrics/expand"}`)
+	graphiteMetricsExpandErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/metrics/expand"}`)
+
+	graphiteMetricsIndexRequests = metrics.NewCounter(`vm_http_requests_total{path="/metrics/index.json"}`)
+	graphiteMetricsIndexErrors   = metrics.NewCounter(`vm_http_request_errors_total{path="/metrics/index.json"}`)
+
 	rulesRequests    = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`)
 	alertsRequests   = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/alerts"}`)
 	metadataRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/metadata"}`)
diff --git a/app/vmselect/netstorage/netstorage.go b/app/vmselect/netstorage/netstorage.go
index b5264008f..63fa24ae1 100644
--- a/app/vmselect/netstorage/netstorage.go
+++ b/app/vmselect/netstorage/netstorage.go
@@ -8,8 +8,8 @@ import (
 	"runtime"
 	"sort"
 	"sync"
-	"time"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
@@ -20,9 +20,10 @@ import (
 )
 
 var (
-	maxTagKeysPerSearch   = flag.Int("search.maxTagKeys", 100e3, "The maximum number of tag keys returned per search")
-	maxTagValuesPerSearch = flag.Int("search.maxTagValues", 100e3, "The maximum number of tag values returned per search")
-	maxMetricsPerSearch   = flag.Int("search.maxUniqueTimeseries", 300e3, "The maximum number of unique time series each search can scan")
+	maxTagKeysPerSearch          = flag.Int("search.maxTagKeys", 100e3, "The maximum number of tag keys returned from /api/v1/labels")
+	maxTagValuesPerSearch        = flag.Int("search.maxTagValues", 100e3, "The maximum number of tag values returned from /api/v1/label/<label_name>/values")
+	maxTagValueSuffixesPerSearch = flag.Int("search.maxTagValueSuffixesPerSearch", 100e3, "The maximum number of tag value suffixes returned from /metrics/find")
+	maxMetricsPerSearch          = flag.Int("search.maxUniqueTimeseries", 300e3, "The maximum number of unique time series each search can scan")
 )
 
 // Result is a single timeseries result.
@@ -52,7 +53,7 @@ func (r *Result) reset() {
 type Results struct {
 	tr        storage.TimeRange
 	fetchData bool
-	deadline  Deadline
+	deadline  searchutils.Deadline
 
 	packedTimeseries []packedTimeseries
 	sr               *storage.Search
@@ -457,11 +458,11 @@ func DeleteSeries(sq *storage.SearchQuery) (int, error) {
 }
 
 // GetLabels returns labels until the given deadline.
-func GetLabels(deadline Deadline) ([]string, error) {
+func GetLabels(deadline searchutils.Deadline) ([]string, error) {
 	if deadline.Exceeded() {
 		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
-	labels, err := vmstorage.SearchTagKeys(*maxTagKeysPerSearch, deadline.deadline)
+	labels, err := vmstorage.SearchTagKeys(*maxTagKeysPerSearch, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("error during labels search: %w", err)
 	}
@@ -481,7 +482,7 @@ func GetLabels(deadline Deadline) ([]string, error) {
 
 // GetLabelValues returns label values for the given labelName
 // until the given deadline.
-func GetLabelValues(labelName string, deadline Deadline) ([]string, error) {
+func GetLabelValues(labelName string, deadline searchutils.Deadline) ([]string, error) {
 	if deadline.Exceeded() {
 		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
@@ -490,7 +491,7 @@ func GetLabelValues(labelName string, deadline Deadline) ([]string, error) {
 	}
 
 	// Search for tag values
-	labelValues, err := vmstorage.SearchTagValues([]byte(labelName), *maxTagValuesPerSearch, deadline.deadline)
+	labelValues, err := vmstorage.SearchTagValues([]byte(labelName), *maxTagValuesPerSearch, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("error during label values search for labelName=%q: %w", labelName, err)
 	}
@@ -501,12 +502,27 @@ func GetLabelValues(labelName string, deadline Deadline) ([]string, error) {
 	return labelValues, nil
 }
 
+// GetTagValueSuffixes returns tag value suffixes for the given tagKey and the given tagValuePrefix.
+//
+// It can be used for implementing https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find
+func GetTagValueSuffixes(tr storage.TimeRange, tagKey, tagValuePrefix string, delimiter byte, deadline searchutils.Deadline) ([]string, error) {
+	if deadline.Exceeded() {
+		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
+	}
+	suffixes, err := vmstorage.SearchTagValueSuffixes(tr, []byte(tagKey), []byte(tagValuePrefix), delimiter, *maxTagValueSuffixesPerSearch, deadline.Deadline())
+	if err != nil {
+		return nil, fmt.Errorf("error during search for suffixes for tagKey=%q, tagValuePrefix=%q, delimiter=%c on time range %s: %w",
+			tagKey, tagValuePrefix, delimiter, tr.String(), err)
+	}
+	return suffixes, nil
+}
+
 // GetLabelEntries returns all the label entries until the given deadline.
-func GetLabelEntries(deadline Deadline) ([]storage.TagEntry, error) {
+func GetLabelEntries(deadline searchutils.Deadline) ([]storage.TagEntry, error) {
 	if deadline.Exceeded() {
 		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
-	labelEntries, err := vmstorage.SearchTagEntries(*maxTagKeysPerSearch, *maxTagValuesPerSearch, deadline.deadline)
+	labelEntries, err := vmstorage.SearchTagEntries(*maxTagKeysPerSearch, *maxTagValuesPerSearch, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("error during label entries request: %w", err)
 	}
@@ -532,11 +548,11 @@ func GetLabelEntries(deadline Deadline) ([]storage.TagEntry, error) {
 }
 
 // GetTSDBStatusForDate returns tsdb status according to https://prometheus.io/docs/prometheus/latest/querying/api/#tsdb-stats
-func GetTSDBStatusForDate(deadline Deadline, date uint64, topN int) (*storage.TSDBStatus, error) {
+func GetTSDBStatusForDate(deadline searchutils.Deadline, date uint64, topN int) (*storage.TSDBStatus, error) {
 	if deadline.Exceeded() {
 		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
-	status, err := vmstorage.GetTSDBStatusForDate(date, topN, deadline.deadline)
+	status, err := vmstorage.GetTSDBStatusForDate(date, topN, deadline.Deadline())
 	if err != nil {
 		return nil, fmt.Errorf("error during tsdb status request: %w", err)
 	}
@@ -544,11 +560,11 @@ func GetTSDBStatusForDate(deadline Deadline, date uint64, topN int) (*storage.TS
 }
 
 // GetSeriesCount returns the number of unique series.
-func GetSeriesCount(deadline Deadline) (uint64, error) {
+func GetSeriesCount(deadline searchutils.Deadline) (uint64, error) {
 	if deadline.Exceeded() {
 		return 0, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
-	n, err := vmstorage.GetSeriesCount(deadline.deadline)
+	n, err := vmstorage.GetSeriesCount(deadline.Deadline())
 	if err != nil {
 		return 0, fmt.Errorf("error during series count request: %w", err)
 	}
@@ -573,7 +589,7 @@ var ssPool sync.Pool
 // ProcessSearchQuery performs sq on storage nodes until the given deadline.
 //
 // Results.RunParallel or Results.Cancel must be called on the returned Results.
-func ProcessSearchQuery(sq *storage.SearchQuery, fetchData bool, deadline Deadline) (*Results, error) {
+func ProcessSearchQuery(sq *storage.SearchQuery, fetchData bool, deadline searchutils.Deadline) (*Results, error) {
 	if deadline.Exceeded() {
 		return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
 	}
@@ -595,7 +611,7 @@ func ProcessSearchQuery(sq *storage.SearchQuery, fetchData bool, deadline Deadli
 	defer vmstorage.WG.Done()
 
 	sr := getStorageSearch()
-	maxSeriesCount := sr.Init(vmstorage.Storage, tfss, tr, *maxMetricsPerSearch, deadline.deadline)
+	maxSeriesCount := sr.Init(vmstorage.Storage, tfss, tr, *maxMetricsPerSearch, deadline.Deadline())
 
 	m := make(map[string][]storage.BlockRef, maxSeriesCount)
 	orderedMetricNames := make([]string, 0, maxSeriesCount)
@@ -656,33 +672,3 @@ func setupTfss(tagFilterss [][]storage.TagFilter) ([]*storage.TagFilters, error)
 	}
 	return tfss, nil
 }
-
-// Deadline contains deadline with the corresponding timeout for pretty error messages.
-type Deadline struct {
-	deadline uint64
-
-	timeout  time.Duration
-	flagHint string
-}
-
-// NewDeadline returns deadline for the given timeout.
-//
-// flagHint must contain a hit for command-line flag, which could be used
-// in order to increase timeout.
-func NewDeadline(startTime time.Time, timeout time.Duration, flagHint string) Deadline {
-	return Deadline{
-		deadline: uint64(startTime.Add(timeout).Unix()),
-		timeout:  timeout,
-		flagHint: flagHint,
-	}
-}
-
-// Exceeded returns true if deadline is exceeded.
-func (d *Deadline) Exceeded() bool {
-	return fasttime.UnixTimestamp() > d.deadline
-}
-
-// String returns human-readable string representation for d.
-func (d *Deadline) String() string {
-	return fmt.Sprintf("%.3f seconds; the timeout can be adjusted with `%s` command-line flag", d.timeout.Seconds(), d.flagHint)
-}
diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go
index 72f38957a..4c18563d0 100644
--- a/app/vmselect/prometheus/prometheus.go
+++ b/app/vmselect/prometheus/prometheus.go
@@ -8,12 +8,12 @@ import (
 	"runtime"
 	"sort"
 	"strconv"
-	"strings"
 	"sync"
 	"time"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
@@ -26,12 +26,10 @@ import (
 )
 
 var (
-	latencyOffset = flag.Duration("search.latencyOffset", time.Second*30, "The time when data points become visible in query results after the colection. "+
+	latencyOffset = flag.Duration("search.latencyOffset", time.Second*30, "The time when data points become visible in query results after the collection. "+
 		"Too small value can result in incomplete last points for query results")
-	maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call")
-	maxQueryDuration  = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for search query execution")
-	maxQueryLen       = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
-	maxLookback       = flag.Duration("search.maxLookback", 0, "Synonim to -search.lookback-delta from Prometheus. "+
+	maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
+	maxLookback = flag.Duration("search.maxLookback", 0, "Synonim to -search.lookback-delta from Prometheus. "+
 		"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
 		"See also '-search.maxStalenessInterval' flag, which has the same meaining due to historical reasons")
 	maxStalenessInterval = flag.Duration("search.maxStalenessInterval", 0, "The maximum interval for staleness calculations. "+
@@ -60,15 +58,15 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
 	if lookbackDelta <= 0 {
 		lookbackDelta = defaultStep
 	}
-	start, err := getTime(r, "start", ct-lookbackDelta)
+	start, err := searchutils.GetTime(r, "start", ct-lookbackDelta)
 	if err != nil {
 		return err
 	}
-	end, err := getTime(r, "end", ct)
+	end, err := searchutils.GetTime(r, "end", ct)
 	if err != nil {
 		return err
 	}
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 	if start >= end {
 		start = end - defaultStep
 	}
@@ -129,17 +127,17 @@ func ExportHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 		}
 		matches = []string{match}
 	}
-	start, err := getTime(r, "start", 0)
+	start, err := searchutils.GetTime(r, "start", 0)
 	if err != nil {
 		return err
 	}
-	end, err := getTime(r, "end", ct)
+	end, err := searchutils.GetTime(r, "end", ct)
 	if err != nil {
 		return err
 	}
 	format := r.FormValue("format")
 	maxRowsPerLine := int(fastfloat.ParseInt64BestEffort(r.FormValue("max_rows_per_line")))
-	deadline := getDeadlineForExport(r, startTime)
+	deadline := searchutils.GetDeadlineForExport(r, startTime)
 	if start >= end {
 		end = start + defaultStep
 	}
@@ -152,7 +150,7 @@ func ExportHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 
 var exportDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export"}`)
 
-func exportHandler(w http.ResponseWriter, matches []string, start, end int64, format string, maxRowsPerLine int, deadline netstorage.Deadline) error {
+func exportHandler(w http.ResponseWriter, matches []string, start, end int64, format string, maxRowsPerLine int, deadline searchutils.Deadline) error {
 	writeResponseFunc := WriteExportStdResponse
 	writeLineFunc := func(rs *netstorage.Result, resultsCh chan<- *quicktemplate.ByteBuffer) {
 		bb := quicktemplate.AcquireByteBuffer()
@@ -283,7 +281,7 @@ var deleteDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/
 //
 // See https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values
 func LabelValuesHandler(startTime time.Time, labelName string, w http.ResponseWriter, r *http.Request) error {
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 	if err := r.ParseForm(); err != nil {
 		return fmt.Errorf("cannot parse form values: %w", err)
 	}
@@ -304,11 +302,11 @@ func LabelValuesHandler(startTime time.Time, labelName string, w http.ResponseWr
 			matches = []string{fmt.Sprintf("{%s!=''}", labelName)}
 		}
 		ct := startTime.UnixNano() / 1e6
-		end, err := getTime(r, "end", ct)
+		end, err := searchutils.GetTime(r, "end", ct)
 		if err != nil {
 			return err
 		}
-		start, err := getTime(r, "start", end-defaultStep)
+		start, err := searchutils.GetTime(r, "start", end-defaultStep)
 		if err != nil {
 			return err
 		}
@@ -324,7 +322,7 @@ func LabelValuesHandler(startTime time.Time, labelName string, w http.ResponseWr
 	return nil
 }
 
-func labelValuesWithMatches(labelName string, matches []string, start, end int64, deadline netstorage.Deadline) ([]string, error) {
+func labelValuesWithMatches(labelName string, matches []string, start, end int64, deadline searchutils.Deadline) ([]string, error) {
 	if len(matches) == 0 {
 		logger.Panicf("BUG: matches must be non-empty")
 	}
@@ -385,7 +383,7 @@ var labelValuesDuration = metrics.NewSummary(`vm_request_duration_seconds{path="
 
 // LabelsCountHandler processes /api/v1/labels/count request.
 func LabelsCountHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 	labelEntries, err := netstorage.GetLabelEntries(deadline)
 	if err != nil {
 		return fmt.Errorf(`cannot obtain label entries: %w`, err)
@@ -404,7 +402,7 @@ const secsPerDay = 3600 * 24
 //
 // See https://prometheus.io/docs/prometheus/latest/querying/api/#tsdb-stats
 func TSDBStatusHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 	if err := r.ParseForm(); err != nil {
 		return fmt.Errorf("cannot parse form values: %w", err)
 	}
@@ -448,7 +446,7 @@ var tsdbStatusDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/
 //
 // See https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names
 func LabelsHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 	if err := r.ParseForm(); err != nil {
 		return fmt.Errorf("cannot parse form values: %w", err)
 	}
@@ -467,11 +465,11 @@ func LabelsHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 			matches = []string{"{__name__!=''}"}
 		}
 		ct := startTime.UnixNano() / 1e6
-		end, err := getTime(r, "end", ct)
+		end, err := searchutils.GetTime(r, "end", ct)
 		if err != nil {
 			return err
 		}
-		start, err := getTime(r, "start", end-defaultStep)
+		start, err := searchutils.GetTime(r, "start", end-defaultStep)
 		if err != nil {
 			return err
 		}
@@ -487,7 +485,7 @@ func LabelsHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 	return nil
 }
 
-func labelsWithMatches(matches []string, start, end int64, deadline netstorage.Deadline) ([]string, error) {
+func labelsWithMatches(matches []string, start, end int64, deadline searchutils.Deadline) ([]string, error) {
 	if len(matches) == 0 {
 		logger.Panicf("BUG: matches must be non-empty")
 	}
@@ -536,7 +534,7 @@ var labelsDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/
 
 // SeriesCountHandler processes /api/v1/series/count request.
 func SeriesCountHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 	n, err := netstorage.GetSeriesCount(deadline)
 	if err != nil {
 		return fmt.Errorf("cannot obtain series count: %w", err)
@@ -561,20 +559,20 @@ func SeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 	if len(matches) == 0 {
 		return fmt.Errorf("missing `match[]` arg")
 	}
-	end, err := getTime(r, "end", ct)
+	end, err := searchutils.GetTime(r, "end", ct)
 	if err != nil {
 		return err
 	}
-	// Do not set start to minTimeMsecs by default as Prometheus does,
+	// Do not set start to searchutils.minTimeMsecs by default as Prometheus does,
 	// since this leads to fetching and scanning all the data from the storage,
 	// which can take a lot of time for big storages.
 	// It is better setting start as end-defaultStep by default.
 	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
-	start, err := getTime(r, "start", end-defaultStep)
+	start, err := searchutils.GetTime(r, "start", end-defaultStep)
 	if err != nil {
 		return err
 	}
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 
 	tagFilterss, err := getTagFilterssFromMatches(matches)
 	if err != nil {
@@ -632,7 +630,7 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
 	if len(query) == 0 {
 		return fmt.Errorf("missing `query` arg")
 	}
-	start, err := getTime(r, "time", ct)
+	start, err := searchutils.GetTime(r, "time", ct)
 	if err != nil {
 		return err
 	}
@@ -640,20 +638,20 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
 	if err != nil {
 		return err
 	}
-	step, err := getDuration(r, "step", lookbackDelta)
+	step, err := searchutils.GetDuration(r, "step", lookbackDelta)
 	if err != nil {
 		return err
 	}
 	if step <= 0 {
 		step = defaultStep
 	}
-	deadline := getDeadlineForQuery(r, startTime)
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
 
 	if len(query) > maxQueryLen.N {
 		return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
 	}
 	queryOffset := getLatencyOffsetMilliseconds()
-	if !getBool(r, "nocache") && ct-start < queryOffset {
+	if !searchutils.GetBool(r, "nocache") && ct-start < queryOffset {
 		// Adjust start time only if `nocache` arg isn't set.
 		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/241
 		start = ct - queryOffset
@@ -746,15 +744,15 @@ func QueryRangeHandler(startTime time.Time, w http.ResponseWriter, r *http.Reque
 	if len(query) == 0 {
 		return fmt.Errorf("missing `query` arg")
 	}
-	start, err := getTime(r, "start", ct-defaultStep)
+	start, err := searchutils.GetTime(r, "start", ct-defaultStep)
 	if err != nil {
 		return err
 	}
-	end, err := getTime(r, "end", ct)
+	end, err := searchutils.GetTime(r, "end", ct)
 	if err != nil {
 		return err
 	}
-	step, err := getDuration(r, "step", defaultStep)
+	step, err := searchutils.GetDuration(r, "step", defaultStep)
 	if err != nil {
 		return err
 	}
@@ -766,8 +764,8 @@ func QueryRangeHandler(startTime time.Time, w http.ResponseWriter, r *http.Reque
 }
 
 func queryRangeHandler(startTime time.Time, w http.ResponseWriter, query string, start, end, step int64, r *http.Request, ct int64) error {
-	deadline := getDeadlineForQuery(r, startTime)
-	mayCache := !getBool(r, "nocache")
+	deadline := searchutils.GetDeadlineForQuery(r, startTime)
+	mayCache := !searchutils.GetBool(r, "nocache")
 	lookbackDelta, err := getMaxLookback(r)
 	if err != nil {
 		return err
@@ -887,120 +885,12 @@ func adjustLastPoints(tss []netstorage.Result, start, end int64) []netstorage.Re
 	return tss
 }
 
-func getTime(r *http.Request, argKey string, defaultValue int64) (int64, error) {
-	argValue := r.FormValue(argKey)
-	if len(argValue) == 0 {
-		return defaultValue, nil
-	}
-	secs, err := strconv.ParseFloat(argValue, 64)
-	if err != nil {
-		// Try parsing string format
-		t, err := time.Parse(time.RFC3339, argValue)
-		if err != nil {
-			// Handle Prometheus'-provided minTime and maxTime.
-			// See https://github.com/prometheus/client_golang/issues/614
-			switch argValue {
-			case prometheusMinTimeFormatted:
-				return minTimeMsecs, nil
-			case prometheusMaxTimeFormatted:
-				return maxTimeMsecs, nil
-			}
-			// Try parsing duration relative to the current time
-			d, err1 := metricsql.DurationValue(argValue, 0)
-			if err1 != nil {
-				return 0, fmt.Errorf("cannot parse %q=%q: %w", argKey, argValue, err)
-			}
-			if d > 0 {
-				d = -d
-			}
-			t = time.Now().Add(time.Duration(d) * time.Millisecond)
-		}
-		secs = float64(t.UnixNano()) / 1e9
-	}
-	msecs := int64(secs * 1e3)
-	if msecs < minTimeMsecs {
-		msecs = 0
-	}
-	if msecs > maxTimeMsecs {
-		msecs = maxTimeMsecs
-	}
-	return msecs, nil
-}
-
-var (
-	// These constants were obtained from https://github.com/prometheus/prometheus/blob/91d7175eaac18b00e370965f3a8186cc40bf9f55/web/api/v1/api.go#L442
-	// See https://github.com/prometheus/client_golang/issues/614 for details.
-	prometheusMinTimeFormatted = time.Unix(math.MinInt64/1000+62135596801, 0).UTC().Format(time.RFC3339Nano)
-	prometheusMaxTimeFormatted = time.Unix(math.MaxInt64/1000-62135596801, 999999999).UTC().Format(time.RFC3339Nano)
-)
-
-const (
-	// These values prevent from overflow when storing msec-precision time in int64.
-	minTimeMsecs = 0 // use 0 instead of `int64(-1<<63) / 1e6` because the storage engine doesn't actually support negative time
-	maxTimeMsecs = int64(1<<63-1) / 1e6
-)
-
-func getDuration(r *http.Request, argKey string, defaultValue int64) (int64, error) {
-	argValue := r.FormValue(argKey)
-	if len(argValue) == 0 {
-		return defaultValue, nil
-	}
-	secs, err := strconv.ParseFloat(argValue, 64)
-	if err != nil {
-		// Try parsing string format
-		d, err := metricsql.DurationValue(argValue, 0)
-		if err != nil {
-			return 0, fmt.Errorf("cannot parse %q=%q: %w", argKey, argValue, err)
-		}
-		secs = float64(d) / 1000
-	}
-	msecs := int64(secs * 1e3)
-	if msecs <= 0 || msecs > maxDurationMsecs {
-		return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 0, int64(maxDurationMsecs))
-	}
-	return msecs, nil
-}
-
-const maxDurationMsecs = 100 * 365 * 24 * 3600 * 1000
-
 func getMaxLookback(r *http.Request) (int64, error) {
 	d := maxLookback.Milliseconds()
 	if d == 0 {
 		d = maxStalenessInterval.Milliseconds()
 	}
-	return getDuration(r, "max_lookback", d)
-}
-
-func getDeadlineForQuery(r *http.Request, startTime time.Time) netstorage.Deadline {
-	dMax := maxQueryDuration.Milliseconds()
-	return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxQueryDuration")
-}
-
-func getDeadlineForExport(r *http.Request, startTime time.Time) netstorage.Deadline {
-	dMax := maxExportDuration.Milliseconds()
-	return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxExportDuration")
-}
-
-func getDeadlineWithMaxDuration(r *http.Request, startTime time.Time, dMax int64, flagHint string) netstorage.Deadline {
-	d, err := getDuration(r, "timeout", 0)
-	if err != nil {
-		d = 0
-	}
-	if d <= 0 || d > dMax {
-		d = dMax
-	}
-	timeout := time.Duration(d) * time.Millisecond
-	return netstorage.NewDeadline(startTime, timeout, flagHint)
-}
-
-func getBool(r *http.Request, argKey string) bool {
-	argValue := r.FormValue(argKey)
-	switch strings.ToLower(argValue) {
-	case "", "0", "f", "false", "no":
-		return false
-	default:
-		return true
-	}
+	return searchutils.GetDuration(r, "max_lookback", d)
 }
 
 func getTagFilterssFromMatches(matches []string) ([][]storage.TagFilter, error) {
diff --git a/app/vmselect/promql/eval.go b/app/vmselect/promql/eval.go
index 1ab6b7c1b..2bbdb4d3f 100644
--- a/app/vmselect/promql/eval.go
+++ b/app/vmselect/promql/eval.go
@@ -8,6 +8,7 @@ import (
 	"sync"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
@@ -88,7 +89,7 @@ type EvalConfig struct {
 	// QuotedRemoteAddr contains quoted remote address.
 	QuotedRemoteAddr string
 
-	Deadline netstorage.Deadline
+	Deadline searchutils.Deadline
 
 	MayCache bool
 
diff --git a/app/vmselect/promql/transform.go b/app/vmselect/promql/transform.go
index f900b5199..65b738776 100644
--- a/app/vmselect/promql/transform.go
+++ b/app/vmselect/promql/transform.go
@@ -1030,6 +1030,12 @@ func transformSmoothExponential(tfa *transformFuncArg) ([]*timeseries, error) {
 	rvs := args[0]
 	for _, ts := range rvs {
 		values := skipLeadingNaNs(ts.Values)
+		for i, v := range values {
+			if !math.IsInf(v, 0) {
+				values = values[i:]
+				break
+			}
+		}
 		if len(values) == 0 {
 			continue
 		}
@@ -1040,6 +1046,10 @@ func transformSmoothExponential(tfa *transformFuncArg) ([]*timeseries, error) {
 			if math.IsNaN(v) {
 				continue
 			}
+			if math.IsInf(v, 0) {
+				values[i] = avg
+				continue
+			}
 			sf := sfsX[i]
 			if math.IsNaN(sf) {
 				sf = 1
diff --git a/app/vmselect/searchutils/searchutils.go b/app/vmselect/searchutils/searchutils.go
new file mode 100644
index 000000000..409aed7e8
--- /dev/null
+++ b/app/vmselect/searchutils/searchutils.go
@@ -0,0 +1,167 @@
+package searchutils
+
+import (
+	"flag"
+	"fmt"
+	"math"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
+	"github.com/VictoriaMetrics/metricsql"
+)
+
+var (
+	maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call")
+	maxQueryDuration  = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for search query execution")
+)
+
+// GetTime returns time from the given argKey query arg.
+func GetTime(r *http.Request, argKey string, defaultValue int64) (int64, error) {
+	argValue := r.FormValue(argKey)
+	if len(argValue) == 0 {
+		return defaultValue, nil
+	}
+	secs, err := strconv.ParseFloat(argValue, 64)
+	if err != nil {
+		// Try parsing string format
+		t, err := time.Parse(time.RFC3339, argValue)
+		if err != nil {
+			// Handle Prometheus'-provided minTime and maxTime.
+			// See https://github.com/prometheus/client_golang/issues/614
+			switch argValue {
+			case prometheusMinTimeFormatted:
+				return minTimeMsecs, nil
+			case prometheusMaxTimeFormatted:
+				return maxTimeMsecs, nil
+			}
+			// Try parsing duration relative to the current time
+			d, err1 := metricsql.DurationValue(argValue, 0)
+			if err1 != nil {
+				return 0, fmt.Errorf("cannot parse %q=%q: %w", argKey, argValue, err)
+			}
+			if d > 0 {
+				d = -d
+			}
+			t = time.Now().Add(time.Duration(d) * time.Millisecond)
+		}
+		secs = float64(t.UnixNano()) / 1e9
+	}
+	msecs := int64(secs * 1e3)
+	if msecs < minTimeMsecs {
+		msecs = 0
+	}
+	if msecs > maxTimeMsecs {
+		msecs = maxTimeMsecs
+	}
+	return msecs, nil
+}
+
+var (
+	// These constants were obtained from https://github.com/prometheus/prometheus/blob/91d7175eaac18b00e370965f3a8186cc40bf9f55/web/api/v1/api.go#L442
+	// See https://github.com/prometheus/client_golang/issues/614 for details.
+	prometheusMinTimeFormatted = time.Unix(math.MinInt64/1000+62135596801, 0).UTC().Format(time.RFC3339Nano)
+	prometheusMaxTimeFormatted = time.Unix(math.MaxInt64/1000-62135596801, 999999999).UTC().Format(time.RFC3339Nano)
+)
+
+const (
+	// These values prevent from overflow when storing msec-precision time in int64.
+	minTimeMsecs = 0 // use 0 instead of `int64(-1<<63) / 1e6` because the storage engine doesn't actually support negative time
+	maxTimeMsecs = int64(1<<63-1) / 1e6
+)
+
+// GetDuration returns duration from the given argKey query arg.
+func GetDuration(r *http.Request, argKey string, defaultValue int64) (int64, error) {
+	argValue := r.FormValue(argKey)
+	if len(argValue) == 0 {
+		return defaultValue, nil
+	}
+	secs, err := strconv.ParseFloat(argValue, 64)
+	if err != nil {
+		// Try parsing string format
+		d, err := metricsql.DurationValue(argValue, 0)
+		if err != nil {
+			return 0, fmt.Errorf("cannot parse %q=%q: %w", argKey, argValue, err)
+		}
+		secs = float64(d) / 1000
+	}
+	msecs := int64(secs * 1e3)
+	if msecs <= 0 || msecs > maxDurationMsecs {
+		return 0, fmt.Errorf("%q=%dms is out of allowed range [%d ... %d]", argKey, msecs, 0, int64(maxDurationMsecs))
+	}
+	return msecs, nil
+}
+
+const maxDurationMsecs = 100 * 365 * 24 * 3600 * 1000
+
+// GetDeadlineForQuery returns deadline for the given query r.
+func GetDeadlineForQuery(r *http.Request, startTime time.Time) Deadline {
+	dMax := maxQueryDuration.Milliseconds()
+	return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxQueryDuration")
+}
+
+// GetDeadlineForExport returns deadline for the given request to /api/v1/export.
+func GetDeadlineForExport(r *http.Request, startTime time.Time) Deadline {
+	dMax := maxExportDuration.Milliseconds()
+	return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxExportDuration")
+}
+
+func getDeadlineWithMaxDuration(r *http.Request, startTime time.Time, dMax int64, flagHint string) Deadline {
+	d, err := GetDuration(r, "timeout", 0)
+	if err != nil {
+		d = 0
+	}
+	if d <= 0 || d > dMax {
+		d = dMax
+	}
+	timeout := time.Duration(d) * time.Millisecond
+	return NewDeadline(startTime, timeout, flagHint)
+}
+
+// GetBool returns boolean value from the given argKey query arg.
+func GetBool(r *http.Request, argKey string) bool {
+	argValue := r.FormValue(argKey)
+	switch strings.ToLower(argValue) {
+	case "", "0", "f", "false", "no":
+		return false
+	default:
+		return true
+	}
+}
+
+// Deadline contains deadline with the corresponding timeout for pretty error messages.
+type Deadline struct {
+	deadline uint64
+
+	timeout  time.Duration
+	flagHint string
+}
+
+// NewDeadline returns deadline for the given timeout.
+//
+// flagHint must contain a hit for command-line flag, which could be used
+// in order to increase timeout.
+func NewDeadline(startTime time.Time, timeout time.Duration, flagHint string) Deadline {
+	return Deadline{
+		deadline: uint64(startTime.Add(timeout).Unix()),
+		timeout:  timeout,
+		flagHint: flagHint,
+	}
+}
+
+// Exceeded returns true if deadline is exceeded.
+func (d *Deadline) Exceeded() bool {
+	return fasttime.UnixTimestamp() > d.deadline
+}
+
+// Deadline returns deadline in unix timestamp seconds.
+func (d *Deadline) Deadline() uint64 {
+	return d.deadline
+}
+
+// String returns human-readable string representation for d.
+func (d *Deadline) String() string {
+	return fmt.Sprintf("%.3f seconds; the timeout can be adjusted with `%s` command-line flag", d.timeout.Seconds(), d.flagHint)
+}
diff --git a/app/vmstorage/main.go b/app/vmstorage/main.go
index ad39a3ed6..abd1ed9ec 100644
--- a/app/vmstorage/main.go
+++ b/app/vmstorage/main.go
@@ -132,6 +132,16 @@ func SearchTagValues(tagKey []byte, maxTagValues int, deadline uint64) ([]string
 	return values, err
 }
 
+// SearchTagValueSuffixes returns all the tag value suffixes for the given tagKey and tagValuePrefix on the given tr.
+//
+// This allows implementing https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find or similar APIs.
+func SearchTagValueSuffixes(tr storage.TimeRange, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int, deadline uint64) ([]string, error) {
+	WG.Add(1)
+	suffixes, err := Storage.SearchTagValueSuffixes(tr, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes, deadline)
+	WG.Done()
+	return suffixes, err
+}
+
 // SearchTagEntries searches for tag entries.
 func SearchTagEntries(maxTagKeys, maxTagValues int, deadline uint64) ([]storage.TagEntry, error) {
 	WG.Add(1)
@@ -456,6 +466,13 @@ func registerStorageMetrics() {
 		return float64(m().SlowMetricNameLoads)
 	})
 
+	metrics.NewGauge(`vm_timestamps_blocks_merged_total`, func() float64 {
+		return float64(m().TimestampsBlocksMerged)
+	})
+	metrics.NewGauge(`vm_timestamps_bytes_saved_total`, func() float64 {
+		return float64(m().TimestampsBytesSaved)
+	})
+
 	metrics.NewGauge(`vm_rows{type="storage/big"}`, func() float64 {
 		return float64(tm().BigRowsCount)
 	})
diff --git a/deployment/docker/Makefile b/deployment/docker/Makefile
index 927db7f91..c1368a7e3 100644
--- a/deployment/docker/Makefile
+++ b/deployment/docker/Makefile
@@ -4,7 +4,7 @@ DOCKER_NAMESPACE := victoriametrics
 
 ROOT_IMAGE ?= alpine:3.12
 CERTS_IMAGE := alpine:3.12
-GO_BUILDER_IMAGE := golang:1.15.1
+GO_BUILDER_IMAGE := golang:1.15.2
 BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr : _)
 BASE_IMAGE := local/base:1.1.1-$(shell echo $(ROOT_IMAGE) | tr : _)-$(shell echo $(CERTS_IMAGE) | tr : _)
 
diff --git a/docs/Cluster-VictoriaMetrics.md b/docs/Cluster-VictoriaMetrics.md
index bff3e1400..507231b42 100644
--- a/docs/Cluster-VictoriaMetrics.md
+++ b/docs/Cluster-VictoriaMetrics.md
@@ -180,7 +180,7 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr
      - `prometheus/api/v1/import/csv` - for importing arbitrary CSV data. See [these docs](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md#how-to-import-csv-data) for details.
      - `prometheus/api/v1/import/prometheus` - for importing data in Prometheus exposition format. See [these docs](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md#how-to-import-data-in-prometheus-exposition-format) for details.
 
-* URLs for querying: `http://<vmselect>:8481/select/<accountID>/prometheus/<suffix>`, where:
+* URLs for [Prmetheus querying API](https://prometheus.io/docs/prometheus/latest/querying/api/): `http://<vmselect>:8481/select/<accountID>/prometheus/<suffix>`, where:
   - `<accountID>` is an arbitrary number identifying data namespace for the query (aka tenant)
   - `<suffix>` may have the following values:
     - `api/v1/query` - performs [PromQL instant query](https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries).
@@ -194,6 +194,13 @@ or [an alternative dashboard for VictoriaMetrics cluster](https://grafana.com/gr
     - `api/v1/status/active_queries` - for currently executed active queries. Note that every `vmselect` maintains an independent list of active queries,
       which is returned in the response.
 
+* URLs for [Graphite Metrics API](https://graphite-api.readthedocs.io/en/latest/api.html#the-metrics-api): `http://<vmselect>:8481/select/<accountID>/graphite/<suffix>`, where:
+    - `<accountID>` is an arbitrary number identifying data namespace for query (aka tenant)
+    - `<suffix>` may have the following values:
+      - `metrics/find` - searches Graphite metrics. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find).
+      - `metrics/expand` - expands Graphite metrics. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand).
+      - `metrics/index.json` - returns all the metric names. See [these docs](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json).
+
 * URL for time series deletion: `http://<vmselect>:8481/delete/<accountID>/prometheus/api/v1/admin/tsdb/delete_series?match[]=<timeseries_selector_for_delete>`.
   Note that the `delete_series` handler should be used only in exceptional cases such as deletion of accidentally ingested incorrect time series. It shouldn't
   be used on a regular basis, since it carries non-zero overhead.
diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md
index 1b5c93317..dad59993d 100644
--- a/docs/MetricsQL.md
+++ b/docs/MetricsQL.md
@@ -126,7 +126,7 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g
   for the given `phi` in the range `[0..1]`.
 - `last_over_time(m[d])` - returns the last value for `m` on the time range `d`.
 - `first_over_time(m[d])` - returns the first value for `m` on the time range `d`.
-- `outliersk(N, q) by (group)` - returns up to `N` outlier time series for `q` in every `group`. Outlier time series have the highest deviation from the `median(m)`.
+- `outliersk(N, q) by (group)` - returns up to `N` outlier time series for `q` in every `group`. Outlier time series have the highest deviation from the `median(q)`.
   This aggregate function is useful to detect anomalies across groups of similar time series.
 - `ascent_over_time(m[d])` - returns the sum of positive deltas between adjancent data points in `m` over `d`. Useful for tracking height gains in GPS track.
 - `descent_over_time(m[d])` - returns the absolute sum of negative deltas between adjancent data points in `m` over `d`. Useful for tracking height loss in GPS track.
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index 75f67b0d7..6f9421ca7 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -103,6 +103,8 @@ See [features available for enterprise customers](https://github.com/VictoriaMet
 * [How to import data in Prometheus exposition format](#how-to-import-data-in-prometheus-exposition-format)
 * [How to import CSV data](#how-to-import-csv-data)
 * [Prometheus querying API usage](#prometheus-querying-api-usage)
+  * [Prometheus querying API enhancements](#prometheus-querying-api-enhancements)
+* [Graphite Metrics API usage](#graphite-metrics-api-usage)
 * [How to build from sources](#how-to-build-from-sources)
   * [Development build](#development-build)
   * [Production build](#production-build)
@@ -392,9 +394,11 @@ The `/api/v1/export` endpoint should return the following response:
 
 ### Querying Graphite data
 
-Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read either via
-[Prometheus querying API](#prometheus-querying-api-usage)
-or via [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml).
+Data sent to VictoriaMetrics via `Graphite plaintext protocol` may be read via the following APIs:
+
+* [Prometheus querying API](#prometheus-querying-api-usage)
+* Metric names can be explored via [Graphite metrics API](#graphite-metrics-api-usage)
+* [go-graphite/carbonapi](https://github.com/go-graphite/carbonapi/blob/master/cmd/carbonapi/carbonapi.example.prometheus.yaml)
 
 ### How to send data from OpenTSDB-compatible agents
 
@@ -547,6 +551,9 @@ It should return something like the following:
 Extra labels may be added to all the imported metrics by passing `extra_label=name=value` query args.
 For example, `/api/v1/import/prometheus?extra_label=foo=bar` would add `{foo="bar"}` label to all the imported metrics.
 
+If timestamp is missing in `<metric> <value> <timestamp>` Prometheus exposition format line, then the current timestamp is used during data ingestion.
+It can be overriden by passing unix timestamp in *milliseconds* via `timestamp` query arg. For example, `/api/v1/import/prometheus?timestamp=1594370496905`.
+
 VictoriaMetrics accepts arbitrary number of lines in a single request to `/api/v1/import/prometheus`, i.e. it supports data streaming.
 
 VictoriaMetrics also may scrape Prometheus targets - see [these docs](#how-to-scrape-prometheus-exporters-such-as-node-exporter).
@@ -585,6 +592,21 @@ Additionally VictoriaMetrics provides the following handlers:
 * `/api/v1/labels/count` - it returns a list of `label: values_count` entries. It can be used for determining labels with the maximum number of values.
 * `/api/v1/status/active_queries` - it returns a list of currently running queries.
 
+
+### Graphite Metrics API usage
+
+VictoriaMetrics supports the following handlers from [Graphite Metrics API](https://graphite-api.readthedocs.io/en/latest/api.html#the-metrics-api):
+
+* [/metrics/find](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find)
+* [/metrics/expand](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-expand)
+* [/metrics/index.json](https://graphite-api.readthedocs.io/en/latest/api.html#metrics-index-json)
+
+VictoriaMetrics accepts the following additional query args at `/metrics/find` and `/metrics/expand`:
+  * `label` - for selecting arbitrary label values. By default `label=__name__`, i.e. metric names are selected.
+  * `delimiter` - for using different delimiters in metric name hierachy. For example, `/metrics/find?delimiter=_&query=node_*` would return all the metric name prefixes
+    that start with `node_`. By default `delimiter=.`.
+
+
 ### How to build from sources
 
 We recommend using either [binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) or
diff --git a/go.mod b/go.mod
index 3e4999eeb..35d15e72b 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/VictoriaMetrics/fasthttp v1.0.5
 	github.com/VictoriaMetrics/metrics v1.12.3
 	github.com/VictoriaMetrics/metricsql v0.5.1
-	github.com/aws/aws-sdk-go v1.34.18
+	github.com/aws/aws-sdk-go v1.34.20
 	github.com/cespare/xxhash/v2 v2.1.1
 	github.com/golang/snappy v0.0.1
 	github.com/klauspost/compress v1.11.0
@@ -21,10 +21,11 @@ require (
 	github.com/valyala/quicktemplate v1.6.2
 	golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
-	golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f
-	golang.org/x/tools v0.0.0-20200904185747-39188db58858 // indirect
+	golang.org/x/sys v0.0.0-20200909081042-eff7692f9009
+	golang.org/x/tools v0.0.0-20200909210914-44a2922940c2 // indirect
 	google.golang.org/api v0.31.0
 	google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
+	google.golang.org/grpc v1.32.0 // indirect
 	gopkg.in/yaml.v2 v2.3.0
 )
 
diff --git a/go.sum b/go.sum
index 22756566b..096c69d4a 100644
--- a/go.sum
+++ b/go.sum
@@ -63,8 +63,8 @@ github.com/VictoriaMetrics/metricsql v0.5.1/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/aws/aws-sdk-go v1.34.18 h1:Mo/Clq3u1dQFzpg8YQqBii8m+Vl3fWIfHi6kXs5wpuM=
-github.com/aws/aws-sdk-go v1.34.18/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.34.20 h1:D9otznteZZyN5pRyFETqveYia/85Xzk7+RaPGB1I9fE=
+github.com/aws/aws-sdk-go v1.34.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -309,8 +309,8 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -363,8 +363,8 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200827163409-021d7c6f1ec3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858 h1:xLt+iB5ksWcZVxqc+g9K41ZHy+6MKWfXCDsjSThnsPA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20200909210914-44a2922940c2 h1:daAzF/Ytp6YSqJDu1hZJthJIhOrsAa7UbIkziU1t0K4=
+golang.org/x/tools v0.0.0-20200909210914-44a2922940c2/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -445,6 +445,8 @@ google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs=
 google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
+google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 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=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/lib/promscrape/discovery/kubernetes/endpointslices.go b/lib/promscrape/discovery/kubernetes/endpointslices.go
new file mode 100644
index 000000000..44ab4f124
--- /dev/null
+++ b/lib/promscrape/discovery/kubernetes/endpointslices.go
@@ -0,0 +1,212 @@
+package kubernetes
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+// getEndpointSlicesLabels returns labels for k8s endpointSlices obtained from the given cfg.
+func getEndpointSlicesLabels(cfg *apiConfig) ([]map[string]string, error) {
+	eps, err := getEndpointSlices(cfg)
+	if err != nil {
+		return nil, err
+	}
+	pods, err := getPods(cfg)
+	if err != nil {
+		return nil, err
+	}
+	svcs, err := getServices(cfg)
+	if err != nil {
+		return nil, err
+	}
+	var ms []map[string]string
+	for _, ep := range eps {
+		ms = ep.appendTargetLabels(ms, pods, svcs)
+	}
+
+	return ms, nil
+}
+
+// getEndpointSlices retrieves endpointSlice with given apiConfig
+func getEndpointSlices(cfg *apiConfig) ([]EndpointSlice, error) {
+	if len(cfg.namespaces) == 0 {
+		return getEndpointSlicesByPath(cfg, "/apis/discovery.k8s.io/v1beta1/endpointslices")
+	}
+	// Query /api/v1/namespaces/* for each namespace.
+	// This fixes authorization issue at https://github.com/VictoriaMetrics/VictoriaMetrics/issues/432
+	cfgCopy := *cfg
+	namespaces := cfgCopy.namespaces
+	cfgCopy.namespaces = nil
+	cfg = &cfgCopy
+	var result []EndpointSlice
+	for _, ns := range namespaces {
+		path := fmt.Sprintf("/apis/discovery.k8s.io/v1beta1/namespaces/%s/endpointslices", ns)
+		eps, err := getEndpointSlicesByPath(cfg, path)
+		if err != nil {
+			return nil, err
+		}
+		result = append(result, eps...)
+	}
+	return result, nil
+}
+
+// getEndpointSlicesByPath retrieves endpointSlices from k8s api by given path
+func getEndpointSlicesByPath(cfg *apiConfig, path string) ([]EndpointSlice, error) {
+	data, err := getAPIResponse(cfg, "endpointslices", path)
+	if err != nil {
+		return nil, fmt.Errorf("cannot obtain endpointslices data from API server: %w", err)
+	}
+	epl, err := parseEndpointSlicesList(data)
+	if err != nil {
+		return nil, fmt.Errorf("cannot parse endpointslices response from API server: %w", err)
+	}
+	return epl.Items, nil
+
+}
+
+// parseEndpointsList parses EndpointSliceList from data.
+func parseEndpointSlicesList(data []byte) (*EndpointSliceList, error) {
+	var esl EndpointSliceList
+	if err := json.Unmarshal(data, &esl); err != nil {
+		return nil, fmt.Errorf("cannot unmarshal EndpointSliceList from %q: %w", data, err)
+	}
+
+	return &esl, nil
+}
+
+// appendTargetLabels injects labels for endPointSlice to slice map
+// follows TargetRef for enrich labels with pod and service metadata
+func (eps *EndpointSlice) appendTargetLabels(ms []map[string]string, pods []Pod, svcs []Service) []map[string]string {
+	svc := getService(svcs, eps.Metadata.Namespace, eps.Metadata.Name)
+	podPortsSeen := make(map[*Pod][]int)
+	for _, ess := range eps.Endpoints {
+		pod := getPod(pods, ess.TargetRef.Namespace, ess.TargetRef.Name)
+		for _, epp := range eps.Ports {
+			for _, addr := range ess.Addresses {
+				ms = append(ms, getEndpointSliceLabelsForAddressAndPort(podPortsSeen, addr, eps, ess, epp, pod, svc))
+			}
+
+		}
+	}
+
+	// Append labels for skipped ports on seen pods.
+	portSeen := func(port int, ports []int) bool {
+		for _, p := range ports {
+			if p == port {
+				return true
+			}
+		}
+		return false
+	}
+	for p, ports := range podPortsSeen {
+		for _, c := range p.Spec.Containers {
+			for _, cp := range c.Ports {
+				if portSeen(cp.ContainerPort, ports) {
+					continue
+				}
+				addr := discoveryutils.JoinHostPort(p.Status.PodIP, cp.ContainerPort)
+				m := map[string]string{
+					"__address__": addr,
+				}
+				p.appendCommonLabels(m)
+				p.appendContainerLabels(m, c, &cp)
+				ms = append(ms, m)
+			}
+		}
+	}
+	return ms
+
+}
+
+// getEndpointSliceLabelsForAddressAndPort gets labels for endpointSlice
+// from  address, Endpoint and EndpointPort
+// enriches labels with TargetRef
+// pod appended to seen Ports
+// if TargetRef matches
+func getEndpointSliceLabelsForAddressAndPort(podPortsSeen map[*Pod][]int, addr string, eps *EndpointSlice, ea Endpoint, epp EndpointPort, p *Pod, svc *Service) map[string]string {
+	m := getEndpointSliceLabels(eps, addr, ea, epp)
+	if svc != nil {
+		svc.appendCommonLabels(m)
+	}
+	if ea.TargetRef.Kind != "Pod" || p == nil {
+		return m
+	}
+	p.appendCommonLabels(m)
+	for _, c := range p.Spec.Containers {
+		for _, cp := range c.Ports {
+			if cp.ContainerPort == epp.Port {
+				p.appendContainerLabels(m, c, &cp)
+				podPortsSeen[p] = append(podPortsSeen[p], cp.ContainerPort)
+				break
+			}
+		}
+	}
+
+	return m
+}
+
+// //getEndpointSliceLabels builds labels for given EndpointSlice
+func getEndpointSliceLabels(eps *EndpointSlice, addr string, ea Endpoint, epp EndpointPort) map[string]string {
+
+	addr = discoveryutils.JoinHostPort(addr, epp.Port)
+	m := map[string]string{
+		"__address__":                                               addr,
+		"__meta_kubernetes_namespace":                               eps.Metadata.Namespace,
+		"__meta_kubernetes_endpointslice_name":                      eps.Metadata.Name,
+		"__meta_kubernetes_endpointslice_address_type":              eps.AddressType,
+		"__meta_kubernetes_endpointslice_endpoint_conditions_ready": strconv.FormatBool(ea.Conditions.Ready),
+		"__meta_kubernetes_endpointslice_port_name":                 epp.Name,
+		"__meta_kubernetes_endpointslice_port_protocol":             epp.Protocol,
+		"__meta_kubernetes_endpointslice_port":                      strconv.FormatUint(uint64(epp.Port), 10),
+	}
+	if epp.AppProtocol != "" {
+		m["__meta_kubernetes_endpointslice_port_app_protocol"] = epp.AppProtocol
+	}
+	if ea.TargetRef.Kind != "" {
+		m["__meta_kubernetes_endpointslice_address_target_kind"] = ea.TargetRef.Kind
+		m["__meta_kubernetes_endpointslice_address_target_name"] = ea.TargetRef.Name
+	}
+	if ea.Hostname != "" {
+		m["__meta_kubernetes_endpointslice_endpoint_hostname"] = ea.Hostname
+	}
+	for k, v := range ea.Topology {
+		m["__meta_kubernetes_endpointslice_endpoint_topology_"+discoveryutils.SanitizeLabelName(k)] = v
+		m["__meta_kubernetes_endpointslice_endpoint_topology_present_"+discoveryutils.SanitizeLabelName(k)] = "true"
+	}
+	return m
+}
+
+// EndpointSliceList - implements kubernetes endpoint slice list object,
+// that groups service endpoints slices.
+// https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointslice-v1beta1-discovery-k8s-io
+type EndpointSliceList struct {
+	Items []EndpointSlice
+}
+
+// EndpointSlice - implements kubernetes endpoint slice.
+// https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointslice-v1beta1-discovery-k8s-io
+type EndpointSlice struct {
+	Metadata    ObjectMeta
+	Endpoints   []Endpoint
+	AddressType string
+	Ports       []EndpointPort
+}
+
+// Endpoint implements kubernetes object endpoint for endpoint slice.
+// https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpoint-v1beta1-discovery-k8s-io
+type Endpoint struct {
+	Addresses  []string
+	Conditions EndpointConditions
+	Hostname   string
+	TargetRef  ObjectReference
+	Topology   map[string]string
+}
+
+// EndpointConditions implements kubernetes endpoint condition.
+// https://v1-17.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#endpointconditions-v1beta1-discovery-k8s-io
+type EndpointConditions struct {
+	Ready bool
+}
diff --git a/lib/promscrape/discovery/kubernetes/kubernetes.go b/lib/promscrape/discovery/kubernetes/kubernetes.go
index f1844792f..1b8d76809 100644
--- a/lib/promscrape/discovery/kubernetes/kubernetes.go
+++ b/lib/promscrape/discovery/kubernetes/kubernetes.go
@@ -50,6 +50,8 @@ func GetLabels(sdc *SDConfig, baseDir string) ([]map[string]string, error) {
 		return getPodsLabels(cfg)
 	case "endpoints":
 		return getEndpointsLabels(cfg)
+	case "endpointslices":
+		return getEndpointSlicesLabels(cfg)
 	case "ingress":
 		return getIngressesLabels(cfg)
 	default:
diff --git a/lib/protoparser/common/timestamp.go b/lib/protoparser/common/timestamp.go
new file mode 100644
index 000000000..2e51abb91
--- /dev/null
+++ b/lib/protoparser/common/timestamp.go
@@ -0,0 +1,22 @@
+package common
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+)
+
+// GetTimestamp extracts unix timestamp in milliseconds from `timestamp` query arg.
+//
+// It returns 0 if there is no `timestamp` query arg.
+func GetTimestamp(req *http.Request) (int64, error) {
+	ts := req.URL.Query().Get("timestamp")
+	if len(ts) == 0 {
+		return 0, nil
+	}
+	timestamp, err := strconv.ParseInt(ts, 10, 64)
+	if err != nil {
+		return 0, fmt.Errorf("cannot parse `timestamp=%s` query arg: %w", ts, err)
+	}
+	return timestamp, nil
+}
diff --git a/lib/protoparser/prometheus/streamparser.go b/lib/protoparser/prometheus/streamparser.go
index 871960e44..fe17e6a36 100644
--- a/lib/protoparser/prometheus/streamparser.go
+++ b/lib/protoparser/prometheus/streamparser.go
@@ -17,7 +17,7 @@ import (
 // The callback can be called multiple times for streamed data from r.
 //
 // callback shouldn't hold rows after returning.
-func ParseStream(r io.Reader, isGzipped bool, callback func(rows []Row) error) error {
+func ParseStream(r io.Reader, defaultTimestamp int64, isGzipped bool, callback func(rows []Row) error) error {
 	if isGzipped {
 		zr, err := common.GetGzipReader(r)
 		if err != nil {
@@ -28,7 +28,7 @@ func ParseStream(r io.Reader, isGzipped bool, callback func(rows []Row) error) e
 	}
 	ctx := getStreamContext()
 	defer putStreamContext(ctx)
-	for ctx.Read(r) {
+	for ctx.Read(r, defaultTimestamp) {
 		if err := callback(ctx.Rows.Rows); err != nil {
 			return err
 		}
@@ -36,7 +36,7 @@ func ParseStream(r io.Reader, isGzipped bool, callback func(rows []Row) error) e
 	return ctx.Error()
 }
 
-func (ctx *streamContext) Read(r io.Reader) bool {
+func (ctx *streamContext) Read(r io.Reader, defaultTimestamp int64) bool {
 	readCalls.Inc()
 	if ctx.err != nil {
 		return false
@@ -55,11 +55,13 @@ func (ctx *streamContext) Read(r io.Reader) bool {
 	rows := ctx.Rows.Rows
 
 	// Fill missing timestamps with the current timestamp.
-	currentTimestamp := int64(time.Now().UnixNano() / 1e6)
+	if defaultTimestamp <= 0 {
+		defaultTimestamp = int64(time.Now().UnixNano() / 1e6)
+	}
 	for i := range rows {
 		r := &rows[i]
 		if r.Timestamp == 0 {
-			r.Timestamp = currentTimestamp
+			r.Timestamp = defaultTimestamp
 		}
 	}
 	return true
diff --git a/lib/storage/block_stream_reader.go b/lib/storage/block_stream_reader.go
index 9b5e20619..f4aa6fab2 100644
--- a/lib/storage/block_stream_reader.go
+++ b/lib/storage/block_stream_reader.go
@@ -51,6 +51,9 @@ type blockStreamReader struct {
 	valuesBlockOffset     uint64
 	indexBlockOffset      uint64
 
+	prevTimestampsBlockOffset uint64
+	prevTimestampsData        []byte
+
 	indexData           []byte
 	compressedIndexData []byte
 
@@ -87,6 +90,9 @@ func (bsr *blockStreamReader) reset() {
 	bsr.valuesBlockOffset = 0
 	bsr.indexBlockOffset = 0
 
+	bsr.prevTimestampsBlockOffset = 0
+	bsr.prevTimestampsData = bsr.prevTimestampsData[:0]
+
 	bsr.indexData = bsr.indexData[:0]
 	bsr.compressedIndexData = bsr.compressedIndexData[:0]
 
@@ -275,7 +281,13 @@ func (bsr *blockStreamReader) readBlock() error {
 		return fmt.Errorf("invalid MaxTimestamp at block header at offset %d; got %d; cannot be bigger than %d",
 			bsr.prevIndexBlockOffset(), bsr.Block.bh.MaxTimestamp, bsr.ph.MaxTimestamp)
 	}
-	if bsr.Block.bh.TimestampsBlockOffset != bsr.timestampsBlockOffset {
+	usePrevTimestamps := len(bsr.prevTimestampsData) > 0 && bsr.Block.bh.TimestampsBlockOffset == bsr.prevTimestampsBlockOffset
+	if usePrevTimestamps {
+		if int(bsr.Block.bh.TimestampsBlockSize) != len(bsr.prevTimestampsData) {
+			return fmt.Errorf("invalid TimestampsBlockSize at block header at offset %d; got %d; want %d",
+				bsr.prevIndexBlockOffset(), bsr.Block.bh.TimestampsBlockSize, len(bsr.prevTimestampsData))
+		}
+	} else if bsr.Block.bh.TimestampsBlockOffset != bsr.timestampsBlockOffset {
 		return fmt.Errorf("invalid TimestampsBlockOffset at block header at offset %d; got %d; want %d",
 			bsr.prevIndexBlockOffset(), bsr.Block.bh.TimestampsBlockOffset, bsr.timestampsBlockOffset)
 	}
@@ -285,9 +297,15 @@ func (bsr *blockStreamReader) readBlock() error {
 	}
 
 	// Read timestamps data.
-	bsr.Block.timestampsData = bytesutil.Resize(bsr.Block.timestampsData, int(bsr.Block.bh.TimestampsBlockSize))
-	if err := fs.ReadFullData(bsr.timestampsReader, bsr.Block.timestampsData); err != nil {
-		return fmt.Errorf("cannot read timestamps block at offset %d: %w", bsr.timestampsBlockOffset, err)
+	if usePrevTimestamps {
+		bsr.Block.timestampsData = append(bsr.Block.timestampsData[:0], bsr.prevTimestampsData...)
+	} else {
+		bsr.Block.timestampsData = bytesutil.Resize(bsr.Block.timestampsData, int(bsr.Block.bh.TimestampsBlockSize))
+		if err := fs.ReadFullData(bsr.timestampsReader, bsr.Block.timestampsData); err != nil {
+			return fmt.Errorf("cannot read timestamps block at offset %d: %w", bsr.timestampsBlockOffset, err)
+		}
+		bsr.prevTimestampsBlockOffset = bsr.timestampsBlockOffset
+		bsr.prevTimestampsData = append(bsr.prevTimestampsData[:0], bsr.Block.timestampsData...)
 	}
 
 	// Read values data.
@@ -297,7 +315,9 @@ func (bsr *blockStreamReader) readBlock() error {
 	}
 
 	// Update offsets.
-	bsr.timestampsBlockOffset += uint64(bsr.Block.bh.TimestampsBlockSize)
+	if !usePrevTimestamps {
+		bsr.timestampsBlockOffset += uint64(bsr.Block.bh.TimestampsBlockSize)
+	}
 	bsr.valuesBlockOffset += uint64(bsr.Block.bh.ValuesBlockSize)
 	bsr.indexBlockHeadersCount++
 
diff --git a/lib/storage/block_stream_writer.go b/lib/storage/block_stream_writer.go
index ce35c0b0b..899facbf7 100644
--- a/lib/storage/block_stream_writer.go
+++ b/lib/storage/block_stream_writer.go
@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"path/filepath"
@@ -38,6 +39,13 @@ type blockStreamWriter struct {
 
 	metaindexData           []byte
 	compressedMetaindexData []byte
+
+	// prevTimestamps* is used as an optimization for reducing disk space usage
+	// when serially written blocks have identical timestamps.
+	// This is usually the case when adjancent blocks contain metrics scraped from the same target,
+	// since such metrics have identical timestamps.
+	prevTimestampsData        []byte
+	prevTimestampsBlockOffset uint64
 }
 
 func (bsw *blockStreamWriter) assertWriteClosers() {
@@ -66,6 +74,9 @@ func (bsw *blockStreamWriter) reset() {
 
 	bsw.metaindexData = bsw.metaindexData[:0]
 	bsw.compressedMetaindexData = bsw.compressedMetaindexData[:0]
+
+	bsw.prevTimestampsData = bsw.prevTimestampsData[:0]
+	bsw.prevTimestampsBlockOffset = 0
 }
 
 // InitFromInmemoryPart initialzes bsw from inmemory part.
@@ -177,22 +188,35 @@ func (bsw *blockStreamWriter) WriteExternalBlock(b *Block, ph *partHeader, rowsM
 	atomic.AddUint64(rowsMerged, uint64(b.rowsCount()))
 	b.deduplicateSamplesDuringMerge()
 	headerData, timestampsData, valuesData := b.MarshalData(bsw.timestampsBlockOffset, bsw.valuesBlockOffset)
-
+	usePrevTimestamps := len(bsw.prevTimestampsData) > 0 && bytes.Equal(timestampsData, bsw.prevTimestampsData)
+	if usePrevTimestamps {
+		// The current timestamps block equals to the previous timestamps block.
+		// Update headerData so it points to the previous timestamps block. This saves disk space.
+		headerData, timestampsData, valuesData = b.MarshalData(bsw.prevTimestampsBlockOffset, bsw.valuesBlockOffset)
+		atomic.AddUint64(&timestampsBlocksMerged, 1)
+		atomic.AddUint64(&timestampsBytesSaved, uint64(len(timestampsData)))
+	}
 	bsw.indexData = append(bsw.indexData, headerData...)
 	bsw.mr.RegisterBlockHeader(&b.bh)
 	if len(bsw.indexData) >= maxBlockSize {
 		bsw.flushIndexData()
 	}
-
-	fs.MustWriteData(bsw.timestampsWriter, timestampsData)
-	bsw.timestampsBlockOffset += uint64(len(timestampsData))
-
+	if !usePrevTimestamps {
+		bsw.prevTimestampsData = append(bsw.prevTimestampsData[:0], timestampsData...)
+		bsw.prevTimestampsBlockOffset = bsw.timestampsBlockOffset
+		fs.MustWriteData(bsw.timestampsWriter, timestampsData)
+		bsw.timestampsBlockOffset += uint64(len(timestampsData))
+	}
 	fs.MustWriteData(bsw.valuesWriter, valuesData)
 	bsw.valuesBlockOffset += uint64(len(valuesData))
-
 	updatePartHeader(b, ph)
 }
 
+var (
+	timestampsBlocksMerged uint64
+	timestampsBytesSaved   uint64
+)
+
 func updatePartHeader(b *Block, ph *partHeader) {
 	ph.BlocksCount++
 	ph.RowsCount += uint64(b.bh.RowsCount)
diff --git a/lib/storage/index_db.go b/lib/storage/index_db.go
index e27a1717d..528effd7c 100644
--- a/lib/storage/index_db.go
+++ b/lib/storage/index_db.go
@@ -901,6 +901,152 @@ func (is *indexSearch) searchTagValues(tvs map[string]struct{}, tagKey []byte, m
 	return nil
 }
 
+// SearchTagValueSuffixes returns all the tag value suffixes for the given tagKey and tagValuePrefix on the given tr.
+//
+// This allows implementing https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find or similar APIs.
+func (db *indexDB) SearchTagValueSuffixes(tr TimeRange, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int, deadline uint64) ([]string, error) {
+	// TODO: cache results?
+
+	tvss := make(map[string]struct{})
+	is := db.getIndexSearch(deadline)
+	err := is.searchTagValueSuffixesForTimeRange(tvss, tr, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes)
+	db.putIndexSearch(is)
+	if err != nil {
+		return nil, err
+	}
+	ok := db.doExtDB(func(extDB *indexDB) {
+		is := extDB.getIndexSearch(deadline)
+		err = is.searchTagValueSuffixesForTimeRange(tvss, tr, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes)
+		extDB.putIndexSearch(is)
+	})
+	if ok && err != nil {
+		return nil, err
+	}
+
+	suffixes := make([]string, 0, len(tvss))
+	for suffix := range tvss {
+		// Do not skip empty suffixes, since they may represent leaf tag values.
+		suffixes = append(suffixes, suffix)
+	}
+	// Do not sort suffixes, since they must be sorted by vmselect.
+	return suffixes, nil
+}
+
+func (is *indexSearch) searchTagValueSuffixesForTimeRange(tvss map[string]struct{}, tr TimeRange, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int) error {
+	minDate := uint64(tr.MinTimestamp) / msecPerDay
+	maxDate := uint64(tr.MaxTimestamp) / msecPerDay
+	if maxDate-minDate > maxDaysForDateMetricIDs {
+		return is.searchTagValueSuffixesAll(tvss, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes)
+	}
+	// Query over multiple days in parallel.
+	var wg sync.WaitGroup
+	var errGlobal error
+	var mu sync.Mutex // protects tvss + errGlobal from concurrent access below.
+	for minDate <= maxDate {
+		wg.Add(1)
+		go func(date uint64) {
+			defer wg.Done()
+			tvssLocal := make(map[string]struct{})
+			isLocal := is.db.getIndexSearch(is.deadline)
+			defer is.db.putIndexSearch(isLocal)
+			err := isLocal.searchTagValueSuffixesForDate(tvssLocal, date, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes)
+			mu.Lock()
+			defer mu.Unlock()
+			if errGlobal != nil {
+				return
+			}
+			if err != nil {
+				errGlobal = err
+				return
+			}
+			for k := range tvssLocal {
+				tvss[k] = struct{}{}
+			}
+		}(minDate)
+		minDate++
+	}
+	wg.Wait()
+	return errGlobal
+}
+
+func (is *indexSearch) searchTagValueSuffixesAll(tvss map[string]struct{}, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int) error {
+	kb := &is.kb
+	nsPrefix := byte(nsPrefixTagToMetricIDs)
+	kb.B = is.marshalCommonPrefix(kb.B[:0], nsPrefix)
+	kb.B = marshalTagValue(kb.B, tagKey)
+	kb.B = marshalTagValue(kb.B, tagValuePrefix)
+	kb.B = kb.B[:len(kb.B)-1] // remove tagSeparatorChar from the end of kb.B
+	prefix := append([]byte(nil), kb.B...)
+	return is.searchTagValueSuffixesForPrefix(tvss, nsPrefix, prefix, tagValuePrefix, delimiter, maxTagValueSuffixes)
+}
+
+func (is *indexSearch) searchTagValueSuffixesForDate(tvss map[string]struct{}, date uint64, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int) error {
+	nsPrefix := byte(nsPrefixDateTagToMetricIDs)
+	kb := &is.kb
+	kb.B = is.marshalCommonPrefix(kb.B[:0], nsPrefix)
+	kb.B = encoding.MarshalUint64(kb.B, date)
+	kb.B = marshalTagValue(kb.B, tagKey)
+	kb.B = marshalTagValue(kb.B, tagValuePrefix)
+	kb.B = kb.B[:len(kb.B)-1] // remove tagSeparatorChar from the end of kb.B
+	prefix := append([]byte(nil), kb.B...)
+	return is.searchTagValueSuffixesForPrefix(tvss, nsPrefix, prefix, tagValuePrefix, delimiter, maxTagValueSuffixes)
+}
+
+func (is *indexSearch) searchTagValueSuffixesForPrefix(tvss map[string]struct{}, nsPrefix byte, prefix, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int) error {
+	kb := &is.kb
+	ts := &is.ts
+	mp := &is.mp
+	mp.Reset()
+	dmis := is.db.getDeletedMetricIDs()
+	loopsPaceLimiter := 0
+	ts.Seek(prefix)
+	for len(tvss) < maxTagValueSuffixes && ts.NextItem() {
+		if loopsPaceLimiter&paceLimiterFastIterationsMask == 0 {
+			if err := checkSearchDeadlineAndPace(is.deadline); err != nil {
+				return err
+			}
+		}
+		loopsPaceLimiter++
+		item := ts.Item
+		if !bytes.HasPrefix(item, prefix) {
+			break
+		}
+		if err := mp.Init(item, nsPrefix); err != nil {
+			return err
+		}
+		if mp.IsDeletedTag(dmis) {
+			continue
+		}
+		tagValue := mp.Tag.Value
+		if !bytes.HasPrefix(tagValue, tagValuePrefix) {
+			continue
+		}
+		suffix := tagValue[len(tagValuePrefix):]
+		n := bytes.IndexByte(suffix, delimiter)
+		if n < 0 {
+			// Found leaf tag value that doesn't have delimiters after the given tagValuePrefix.
+			tvss[string(suffix)] = struct{}{}
+			continue
+		}
+		// Found non-leaf tag value. Extract suffix that end with the given delimiter.
+		suffix = suffix[:n+1]
+		tvss[string(suffix)] = struct{}{}
+		if suffix[len(suffix)-1] == 255 {
+			continue
+		}
+		// Search for the next suffix
+		suffix[len(suffix)-1]++
+		kb.B = append(kb.B[:0], prefix...)
+		kb.B = marshalTagValue(kb.B, suffix)
+		kb.B = kb.B[:len(kb.B)-1] // remove tagSeparatorChar
+		ts.Seek(kb.B)
+	}
+	if err := ts.Error(); err != nil {
+		return fmt.Errorf("error when searching for tag value sufixes for prefix %q: %w", prefix, err)
+	}
+	return nil
+}
+
 // GetSeriesCount returns the approximate number of unique timeseries in the db.
 //
 // It includes the deleted series too and may count the same series
diff --git a/lib/storage/storage.go b/lib/storage/storage.go
index fd9a96067..d038351d3 100644
--- a/lib/storage/storage.go
+++ b/lib/storage/storage.go
@@ -340,6 +340,9 @@ type Metrics struct {
 	SlowPerDayIndexInserts uint64
 	SlowMetricNameLoads    uint64
 
+	TimestampsBlocksMerged uint64
+	TimestampsBytesSaved   uint64
+
 	TSIDCacheSize       uint64
 	TSIDCacheSizeBytes  uint64
 	TSIDCacheRequests   uint64
@@ -405,6 +408,9 @@ func (s *Storage) UpdateMetrics(m *Metrics) {
 	m.SlowPerDayIndexInserts += atomic.LoadUint64(&s.slowPerDayIndexInserts)
 	m.SlowMetricNameLoads += atomic.LoadUint64(&s.slowMetricNameLoads)
 
+	m.TimestampsBlocksMerged = atomic.LoadUint64(&timestampsBlocksMerged)
+	m.TimestampsBytesSaved = atomic.LoadUint64(&timestampsBytesSaved)
+
 	var cs fastcache.Stats
 	s.tsidCache.UpdateStats(&cs)
 	m.TSIDCacheSize += cs.EntriesCount
@@ -923,6 +929,13 @@ func (s *Storage) SearchTagValues(tagKey []byte, maxTagValues int, deadline uint
 	return s.idb().SearchTagValues(tagKey, maxTagValues, deadline)
 }
 
+// SearchTagValueSuffixes returns all the tag value suffixes for the given tagKey and tagValuePrefix on the given tr.
+//
+// This allows implementing https://graphite-api.readthedocs.io/en/latest/api.html#metrics-find or similar APIs.
+func (s *Storage) SearchTagValueSuffixes(tr TimeRange, tagKey, tagValuePrefix []byte, delimiter byte, maxTagValueSuffixes int, deadline uint64) ([]string, error) {
+	return s.idb().SearchTagValueSuffixes(tr, tagKey, tagValuePrefix, delimiter, maxTagValueSuffixes, deadline)
+}
+
 // SearchTagEntries returns a list of (tagName -> tagValues)
 func (s *Storage) SearchTagEntries(maxTagKeys, maxTagValues int, deadline uint64) ([]TagEntry, error) {
 	idb := s.idb()
@@ -1107,6 +1120,11 @@ func (s *Storage) add(rows []rawRow, mrs []MetricRow, precisionBits uint8) ([]ra
 			// doesn't know how to work with them.
 			continue
 		}
+		if math.IsInf(mr.Value, 0) {
+			// Skip Inf values, since they may break precision for already stored data.
+			// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/752
+			continue
+		}
 		if mr.Timestamp < minTimestamp {
 			// Skip rows with too small timestamps outside the retention.
 			if firstWarn == nil {
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 b1f1c13dc..b695f1f79 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
@@ -789,6 +789,7 @@ var awsPartition = partition{
 				"ca-central-1":   endpoint{},
 				"eu-central-1":   endpoint{},
 				"eu-north-1":     endpoint{},
+				"eu-south-1":     endpoint{},
 				"eu-west-1":      endpoint{},
 				"eu-west-2":      endpoint{},
 				"eu-west-3":      endpoint{},
@@ -2929,6 +2930,7 @@ var awsPartition = partition{
 				"ca-central-1":   endpoint{},
 				"eu-central-1":   endpoint{},
 				"eu-north-1":     endpoint{},
+				"eu-south-1":     endpoint{},
 				"eu-west-1":      endpoint{},
 				"eu-west-2":      endpoint{},
 				"eu-west-3":      endpoint{},
@@ -3468,6 +3470,7 @@ var awsPartition = partition{
 				"ca-central-1":   endpoint{},
 				"eu-central-1":   endpoint{},
 				"eu-north-1":     endpoint{},
+				"eu-south-1":     endpoint{},
 				"eu-west-1":      endpoint{},
 				"eu-west-2":      endpoint{},
 				"eu-west-3":      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 6c0a8f4d3..c00cd55fd 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.34.18"
+const SDKVersion = "1.34.20"
diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go
index 5b3af2e6a..ec7e4c4d3 100644
--- a/vendor/golang.org/x/sys/unix/syscall_linux.go
+++ b/vendor/golang.org/x/sys/unix/syscall_linux.go
@@ -136,6 +136,12 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)
 	return openat(dirfd, path, flags|O_LARGEFILE, mode)
 }
 
+//sys	openat2(dirfd int, path string, open_how *OpenHow, size int) (fd int, err error)
+
+func Openat2(dirfd int, path string, how *OpenHow) (fd int, err error) {
+	return openat2(dirfd, path, how, SizeofOpenHow)
+}
+
 //sys	ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error)
 
 func Ppoll(fds []PollFd, timeout *Timespec, sigmask *Sigset_t) (n int, err error) {
diff --git a/vendor/golang.org/x/sys/unix/zsyscall_linux.go b/vendor/golang.org/x/sys/unix/zsyscall_linux.go
index 4eec7a795..2fbbbe5a8 100644
--- a/vendor/golang.org/x/sys/unix/zsyscall_linux.go
+++ b/vendor/golang.org/x/sys/unix/zsyscall_linux.go
@@ -83,6 +83,22 @@ func openat(dirfd int, path string, flags int, mode uint32) (fd int, err error)
 
 // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
 
+func openat2(dirfd int, path string, open_how *OpenHow, size int) (fd int, err error) {
+	var _p0 *byte
+	_p0, err = BytePtrFromString(path)
+	if err != nil {
+		return
+	}
+	r0, _, e1 := Syscall6(SYS_OPENAT2, uintptr(dirfd), uintptr(unsafe.Pointer(_p0)), uintptr(unsafe.Pointer(open_how)), uintptr(size), 0, 0)
+	fd = int(r0)
+	if e1 != 0 {
+		err = errnoErr(e1)
+	}
+	return
+}
+
+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
+
 func ppoll(fds *PollFd, nfds int, timeout *Timespec, sigmask *Sigset_t) (n int, err error) {
 	r0, _, e1 := Syscall6(SYS_PPOLL, uintptr(unsafe.Pointer(fds)), uintptr(nfds), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)), 0, 0)
 	n = int(r0)
diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go
index 1879d5789..68e4974a9 100644
--- a/vendor/golang.org/x/sys/unix/ztypes_linux.go
+++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go
@@ -752,6 +752,22 @@ const (
 	AT_EACCESS = 0x200
 )
 
+type OpenHow struct {
+	Flags   uint64
+	Mode    uint64
+	Resolve uint64
+}
+
+const SizeofOpenHow = 0x18
+
+const (
+	RESOLVE_BENEATH       = 0x8
+	RESOLVE_IN_ROOT       = 0x10
+	RESOLVE_NO_MAGICLINKS = 0x2
+	RESOLVE_NO_SYMLINKS   = 0x4
+	RESOLVE_NO_XDEV       = 0x1
+)
+
 type PollFd struct {
 	Fd      int32
 	Events  int16
diff --git a/vendor/google.golang.org/grpc/.travis.yml b/vendor/google.golang.org/grpc/.travis.yml
index 0e24e59f0..9097478f8 100644
--- a/vendor/google.golang.org/grpc/.travis.yml
+++ b/vendor/google.golang.org/grpc/.travis.yml
@@ -16,8 +16,8 @@ matrix:
     env: GO111MODULE=on
   - go: 1.12.x
     env: GO111MODULE=on
-  - go: 1.9.x
-    env: GAE=1
+  - go: 1.11.x  # Keep until interop tests no longer require Go1.11
+    env: GO111MODULE=on
 
 go_import_path: google.golang.org/grpc
 
@@ -30,13 +30,11 @@ before_install:
 install:
   - try3() { eval "$*" || eval "$*" || eval "$*"; }
   - try3 'if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make testdeps; fi'
-  - if [[ -n "${GAE}" ]]; then source ./install_gae.sh; make testappenginedeps; fi
   - if [[ -n "${VET}" ]]; then ./vet.sh -install; fi
 
 script:
   - set -e
   - if [[ -n "${TESTEXTRAS}" ]]; then examples/examples_test.sh; interop/interop_test.sh; make testsubmodule; exit 0; fi
   - if [[ -n "${VET}" ]]; then ./vet.sh; fi
-  - if [[ -n "${GAE}" ]]; then make testappengine; exit 0; fi
   - if [[ -n "${RACE}" ]]; then make testrace; exit 0; fi
   - make test
diff --git a/vendor/google.golang.org/grpc/CONTRIBUTING.md b/vendor/google.golang.org/grpc/CONTRIBUTING.md
index 4f1567e2f..cd03f8c76 100644
--- a/vendor/google.golang.org/grpc/CONTRIBUTING.md
+++ b/vendor/google.golang.org/grpc/CONTRIBUTING.md
@@ -57,6 +57,5 @@ How to get your contributions merged smoothly and quickly.
   - `make vet` to catch vet errors
   - `make test` to run the tests
   - `make testrace` to run tests in race mode
-  - optional `make testappengine` to run tests with appengine
 
 - Exceptions to the rules can be made if there's a compelling reason for doing so.
diff --git a/vendor/google.golang.org/grpc/Makefile b/vendor/google.golang.org/grpc/Makefile
index 410f7d56d..cf474ae2f 100644
--- a/vendor/google.golang.org/grpc/Makefile
+++ b/vendor/google.golang.org/grpc/Makefile
@@ -21,12 +21,7 @@ test: testdeps
 
 testsubmodule: testdeps
 	cd security/advancedtls && go test -cpu 1,4 -timeout 7m google.golang.org/grpc/security/advancedtls/...
-
-testappengine: testappenginedeps
-	goapp test -cpu 1,4 -timeout 7m google.golang.org/grpc/...
-
-testappenginedeps:
-	goapp get -d -v -t -tags 'appengine appenginevm' google.golang.org/grpc/...
+	cd security/authorization && go test -cpu 1,4 -timeout 7m google.golang.org/grpc/security/authorization/...
 
 testdeps:
 	go get -d -v -t google.golang.org/grpc/...
@@ -53,8 +48,6 @@ vetdeps:
 	deps \
 	proto \
 	test \
-	testappengine \
-	testappenginedeps \
 	testdeps \
 	testrace \
 	updatedeps \
diff --git a/vendor/google.golang.org/grpc/README.md b/vendor/google.golang.org/grpc/README.md
index fef78e4a1..3949a683f 100644
--- a/vendor/google.golang.org/grpc/README.md
+++ b/vendor/google.golang.org/grpc/README.md
@@ -1,7 +1,7 @@
 # gRPC-Go
 
 [![Build Status](https://travis-ci.org/grpc/grpc-go.svg)](https://travis-ci.org/grpc/grpc-go)
-[![GoDoc](https://godoc.org/google.golang.org/grpc?status.svg)][API]
+[![GoDoc](https://pkg.go.dev/badge/google.golang.org/grpc)][API]
 [![GoReportCard](https://goreportcard.com/badge/grpc/grpc-go)](https://goreportcard.com/report/github.com/grpc/grpc-go)
 
 The [Go][] implementation of [gRPC][]: A high performance, open source, general
@@ -131,7 +131,7 @@ the root cause of the connection being closed is on the server side. Turn on
 logging on __both client and server__, and see if there are any transport
 errors.
 
-[API]: https://grpc.io/docs/languages/go/api
+[API]: https://pkg.go.dev/google.golang.org/grpc
 [Go]: https://golang.org
 [Go module]: https://github.com/golang/go/wiki/Modules
 [gRPC]: https://grpc.io
diff --git a/vendor/google.golang.org/grpc/connectivity/connectivity.go b/vendor/google.golang.org/grpc/connectivity/connectivity.go
index 34ec36fbf..0a8d682ac 100644
--- a/vendor/google.golang.org/grpc/connectivity/connectivity.go
+++ b/vendor/google.golang.org/grpc/connectivity/connectivity.go
@@ -27,6 +27,8 @@ import (
 	"google.golang.org/grpc/grpclog"
 )
 
+var logger = grpclog.Component("core")
+
 // State indicates the state of connectivity.
 // It can be the state of a ClientConn or SubConn.
 type State int
@@ -44,7 +46,7 @@ func (s State) String() string {
 	case Shutdown:
 		return "SHUTDOWN"
 	default:
-		grpclog.Errorf("unknown connectivity state: %d", s)
+		logger.Errorf("unknown connectivity state: %d", s)
 		return "Invalid-State"
 	}
 }
diff --git a/vendor/google.golang.org/grpc/credentials/internal/syscallconn_appengine.go b/vendor/google.golang.org/grpc/credentials/internal/syscallconn_appengine.go
deleted file mode 100644
index d4346e9ea..000000000
--- a/vendor/google.golang.org/grpc/credentials/internal/syscallconn_appengine.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// +build appengine
-
-/*
- *
- * Copyright 2018 gRPC authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package internal
-
-import (
-	"net"
-)
-
-// WrapSyscallConn returns newConn on appengine.
-func WrapSyscallConn(rawConn, newConn net.Conn) net.Conn {
-	return newConn
-}
diff --git a/vendor/google.golang.org/grpc/credentials/tls.go b/vendor/google.golang.org/grpc/credentials/tls.go
index 1ba6f3a6b..48384f505 100644
--- a/vendor/google.golang.org/grpc/credentials/tls.go
+++ b/vendor/google.golang.org/grpc/credentials/tls.go
@@ -27,7 +27,6 @@ import (
 	"net"
 	"net/url"
 
-	"google.golang.org/grpc/credentials/internal"
 	credinternal "google.golang.org/grpc/internal/credentials"
 )
 
@@ -73,7 +72,7 @@ func (c tlsCreds) Info() ProtocolInfo {
 
 func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (_ net.Conn, _ AuthInfo, err error) {
 	// use local cfg to avoid clobbering ServerName if using multiple endpoints
-	cfg := cloneTLSConfig(c.config)
+	cfg := credinternal.CloneTLSConfig(c.config)
 	if cfg.ServerName == "" {
 		serverName, _, err := net.SplitHostPort(authority)
 		if err != nil {
@@ -108,7 +107,7 @@ func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawCon
 	if id != nil {
 		tlsInfo.SPIFFEID = id
 	}
-	return internal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
+	return credinternal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
 }
 
 func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error) {
@@ -127,7 +126,7 @@ func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, AuthInfo, error)
 	if id != nil {
 		tlsInfo.SPIFFEID = id
 	}
-	return internal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
+	return credinternal.WrapSyscallConn(rawConn, conn), tlsInfo, nil
 }
 
 func (c *tlsCreds) Clone() TransportCredentials {
@@ -139,23 +138,10 @@ func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
 	return nil
 }
 
-const alpnProtoStrH2 = "h2"
-
-func appendH2ToNextProtos(ps []string) []string {
-	for _, p := range ps {
-		if p == alpnProtoStrH2 {
-			return ps
-		}
-	}
-	ret := make([]string, 0, len(ps)+1)
-	ret = append(ret, ps...)
-	return append(ret, alpnProtoStrH2)
-}
-
 // NewTLS uses c to construct a TransportCredentials based on TLS.
 func NewTLS(c *tls.Config) TransportCredentials {
-	tc := &tlsCreds{cloneTLSConfig(c)}
-	tc.config.NextProtos = appendH2ToNextProtos(tc.config.NextProtos)
+	tc := &tlsCreds{credinternal.CloneTLSConfig(c)}
+	tc.config.NextProtos = credinternal.AppendH2ToNextProtos(tc.config.NextProtos)
 	return tc
 }
 
@@ -242,18 +228,3 @@ var cipherSuiteLookup = map[uint16]string{
 	tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305:    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
 	tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305:  "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
 }
-
-// cloneTLSConfig returns a shallow clone of the exported
-// fields of cfg, ignoring the unexported sync.Once, which
-// contains a mutex and must not be copied.
-//
-// If cfg is nil, a new zero tls.Config is returned.
-//
-// TODO: inline this function if possible.
-func cloneTLSConfig(cfg *tls.Config) *tls.Config {
-	if cfg == nil {
-		return &tls.Config{}
-	}
-
-	return cfg.Clone()
-}
diff --git a/vendor/google.golang.org/grpc/grpclog/component.go b/vendor/google.golang.org/grpc/grpclog/component.go
index b513281a3..8358dd6e2 100644
--- a/vendor/google.golang.org/grpc/grpclog/component.go
+++ b/vendor/google.golang.org/grpc/grpclog/component.go
@@ -100,7 +100,7 @@ func (c *componentData) Fatalln(args ...interface{}) {
 }
 
 func (c *componentData) V(l int) bool {
-	return grpclog.Logger.V(l)
+	return V(l)
 }
 
 // Component creates a new component and returns it for logging. If a component
diff --git a/vendor/google.golang.org/grpc/install_gae.sh b/vendor/google.golang.org/grpc/install_gae.sh
deleted file mode 100644
index 7c7bcada5..000000000
--- a/vendor/google.golang.org/grpc/install_gae.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-TMP=$(mktemp -d /tmp/sdk.XXX) \
-&& curl -o $TMP.zip "https://storage.googleapis.com/appengine-sdks/featured/go_appengine_sdk_linux_amd64-1.9.68.zip" \
-&& unzip -q $TMP.zip -d $TMP \
-&& export PATH="$PATH:$TMP/go_appengine"
diff --git a/vendor/google.golang.org/grpc/internal/channelz/types_linux.go b/vendor/google.golang.org/grpc/internal/channelz/types_linux.go
index 692dd6181..1b1c4cce3 100644
--- a/vendor/google.golang.org/grpc/internal/channelz/types_linux.go
+++ b/vendor/google.golang.org/grpc/internal/channelz/types_linux.go
@@ -1,5 +1,3 @@
-// +build !appengine
-
 /*
  *
  * Copyright 2018 gRPC authors.
diff --git a/vendor/google.golang.org/grpc/internal/channelz/types_nonlinux.go b/vendor/google.golang.org/grpc/internal/channelz/types_nonlinux.go
index 19c2fc521..da7351e97 100644
--- a/vendor/google.golang.org/grpc/internal/channelz/types_nonlinux.go
+++ b/vendor/google.golang.org/grpc/internal/channelz/types_nonlinux.go
@@ -37,6 +37,6 @@ type SocketOptionData struct {
 // Windows OS doesn't support Socket Option
 func (s *SocketOptionData) Getsockopt(fd uintptr) {
 	once.Do(func() {
-		logger.Warning("Channelz: socket options are not supported on non-linux os and appengine.")
+		logger.Warning("Channelz: socket options are not supported on non-linux os.")
 	})
 }
diff --git a/vendor/google.golang.org/grpc/internal/channelz/util_linux.go b/vendor/google.golang.org/grpc/internal/channelz/util_linux.go
index fdf409d55..49432bebf 100644
--- a/vendor/google.golang.org/grpc/internal/channelz/util_linux.go
+++ b/vendor/google.golang.org/grpc/internal/channelz/util_linux.go
@@ -1,4 +1,4 @@
-// +build linux,!appengine
+// +build linux
 
 /*
  *
diff --git a/vendor/google.golang.org/grpc/internal/credentials/go110.go b/vendor/google.golang.org/grpc/internal/credentials/spiffe.go
similarity index 83%
rename from vendor/google.golang.org/grpc/internal/credentials/go110.go
rename to vendor/google.golang.org/grpc/internal/credentials/spiffe.go
index d55b52036..406023035 100644
--- a/vendor/google.golang.org/grpc/internal/credentials/go110.go
+++ b/vendor/google.golang.org/grpc/internal/credentials/spiffe.go
@@ -1,5 +1,3 @@
-// +build go1.10
-
 /*
  *
  * Copyright 2020 gRPC authors.
@@ -30,6 +28,8 @@ import (
 	"google.golang.org/grpc/grpclog"
 )
 
+var logger = grpclog.Component("credentials")
+
 // SPIFFEIDFromState parses the SPIFFE ID from State. If the SPIFFE ID format
 // is invalid, return nil with warning.
 func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
@@ -43,20 +43,20 @@ func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
 		}
 		// From this point, we assume the uri is intended for a SPIFFE ID.
 		if len(uri.String()) > 2048 {
-			grpclog.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes")
+			logger.Warning("invalid SPIFFE ID: total ID length larger than 2048 bytes")
 			return nil
 		}
 		if len(uri.Host) == 0 || len(uri.RawPath) == 0 || len(uri.Path) == 0 {
-			grpclog.Warning("invalid SPIFFE ID: domain or workload ID is empty")
+			logger.Warning("invalid SPIFFE ID: domain or workload ID is empty")
 			return nil
 		}
 		if len(uri.Host) > 255 {
-			grpclog.Warning("invalid SPIFFE ID: domain length larger than 255 characters")
+			logger.Warning("invalid SPIFFE ID: domain length larger than 255 characters")
 			return nil
 		}
 		// A valid SPIFFE certificate can only have exactly one URI SAN field.
 		if len(state.PeerCertificates[0].URIs) > 1 {
-			grpclog.Warning("invalid SPIFFE ID: multiple URI SANs")
+			logger.Warning("invalid SPIFFE ID: multiple URI SANs")
 			return nil
 		}
 		spiffeID = uri
diff --git a/vendor/google.golang.org/grpc/internal/credentials/gobefore110.go b/vendor/google.golang.org/grpc/internal/credentials/spiffe_appengine.go
similarity index 90%
rename from vendor/google.golang.org/grpc/internal/credentials/gobefore110.go
rename to vendor/google.golang.org/grpc/internal/credentials/spiffe_appengine.go
index 743713e19..af6f57719 100644
--- a/vendor/google.golang.org/grpc/internal/credentials/gobefore110.go
+++ b/vendor/google.golang.org/grpc/internal/credentials/spiffe_appengine.go
@@ -1,4 +1,4 @@
-// +build !go1.10
+// +build appengine
 
 /*
  *
@@ -25,7 +25,7 @@ import (
 	"net/url"
 )
 
-//TODO(ZhenLian): delete this file when we remove Go 1.9 tests.
+// SPIFFEIDFromState is a no-op for appengine builds.
 func SPIFFEIDFromState(state tls.ConnectionState) *url.URL {
 	return nil
 }
diff --git a/vendor/google.golang.org/grpc/credentials/internal/syscallconn.go b/vendor/google.golang.org/grpc/internal/credentials/syscallconn.go
similarity index 94%
rename from vendor/google.golang.org/grpc/credentials/internal/syscallconn.go
rename to vendor/google.golang.org/grpc/internal/credentials/syscallconn.go
index 2f4472bec..2919632d6 100644
--- a/vendor/google.golang.org/grpc/credentials/internal/syscallconn.go
+++ b/vendor/google.golang.org/grpc/internal/credentials/syscallconn.go
@@ -1,5 +1,3 @@
-// +build !appengine
-
 /*
  *
  * Copyright 2018 gRPC authors.
@@ -18,8 +16,7 @@
  *
  */
 
-// Package internal contains credentials-internal code.
-package internal
+package credentials
 
 import (
 	"net"
diff --git a/vendor/google.golang.org/grpc/internal/credentials/util.go b/vendor/google.golang.org/grpc/internal/credentials/util.go
new file mode 100644
index 000000000..55664fa46
--- /dev/null
+++ b/vendor/google.golang.org/grpc/internal/credentials/util.go
@@ -0,0 +1,50 @@
+/*
+ *
+ * Copyright 2020 gRPC authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package credentials
+
+import "crypto/tls"
+
+const alpnProtoStrH2 = "h2"
+
+// AppendH2ToNextProtos appends h2 to next protos.
+func AppendH2ToNextProtos(ps []string) []string {
+	for _, p := range ps {
+		if p == alpnProtoStrH2 {
+			return ps
+		}
+	}
+	ret := make([]string, 0, len(ps)+1)
+	ret = append(ret, ps...)
+	return append(ret, alpnProtoStrH2)
+}
+
+// CloneTLSConfig returns a shallow clone of the exported
+// fields of cfg, ignoring the unexported sync.Once, which
+// contains a mutex and must not be copied.
+//
+// If cfg is nil, a new zero tls.Config is returned.
+//
+// TODO: inline this function if possible.
+func CloneTLSConfig(cfg *tls.Config) *tls.Config {
+	if cfg == nil {
+		return &tls.Config{}
+	}
+
+	return cfg.Clone()
+}
diff --git a/vendor/google.golang.org/grpc/internal/serviceconfig/serviceconfig.go b/vendor/google.golang.org/grpc/internal/serviceconfig/serviceconfig.go
index 9b26414d4..af3e2b5f7 100644
--- a/vendor/google.golang.org/grpc/internal/serviceconfig/serviceconfig.go
+++ b/vendor/google.golang.org/grpc/internal/serviceconfig/serviceconfig.go
@@ -28,8 +28,13 @@ import (
 	externalserviceconfig "google.golang.org/grpc/serviceconfig"
 )
 
-// BalancerConfig is the balancer config part that service config's
-// loadBalancingConfig fields can be unmarshalled to. It's a json unmarshaller.
+var logger = grpclog.Component("core")
+
+// BalancerConfig wraps the name and config associated with one load balancing
+// policy. It corresponds to a single entry of the loadBalancingConfig field
+// from ServiceConfig.
+//
+// It implements the json.Unmarshaler interface.
 //
 // https://github.com/grpc/grpc-proto/blob/54713b1e8bc6ed2d4f25fb4dff527842150b91b2/grpc/service_config/service_config.proto#L247
 type BalancerConfig struct {
@@ -39,7 +44,15 @@ type BalancerConfig struct {
 
 type intermediateBalancerConfig []map[string]json.RawMessage
 
-// UnmarshalJSON implements json unmarshaller.
+// UnmarshalJSON implements the json.Unmarshaler interface.
+//
+// ServiceConfig contains a list of loadBalancingConfigs, each with a name and
+// config. This method iterates through that list in order, and stops at the
+// first policy that is supported.
+// - If the config for the first supported policy is invalid, the whole service
+//   config is invalid.
+// - If the list doesn't contain any supported policy, the whole service config
+//   is invalid.
 func (bc *BalancerConfig) UnmarshalJSON(b []byte) error {
 	var ir intermediateBalancerConfig
 	err := json.Unmarshal(b, &ir)
@@ -51,13 +64,16 @@ func (bc *BalancerConfig) UnmarshalJSON(b []byte) error {
 		if len(lbcfg) != 1 {
 			return fmt.Errorf("invalid loadBalancingConfig: entry %v does not contain exactly 1 policy/config pair: %q", i, lbcfg)
 		}
+
 		var (
 			name    string
 			jsonCfg json.RawMessage
 		)
-		// Get the key:value pair from the map.
+		// Get the key:value pair from the map. We have already made sure that
+		// the map contains a single entry.
 		for name, jsonCfg = range lbcfg {
 		}
+
 		builder := balancer.Get(name)
 		if builder == nil {
 			// If the balancer is not registered, move on to the next config.
@@ -69,7 +85,7 @@ func (bc *BalancerConfig) UnmarshalJSON(b []byte) error {
 		parser, ok := builder.(balancer.ConfigParser)
 		if !ok {
 			if string(jsonCfg) != "{}" {
-				grpclog.Warningf("non-empty balancer configuration %q, but balancer does not implement ParseConfig", string(jsonCfg))
+				logger.Warningf("non-empty balancer configuration %q, but balancer does not implement ParseConfig", string(jsonCfg))
 			}
 			// Stop at this, though the builder doesn't support parsing config.
 			return nil
diff --git a/vendor/google.golang.org/grpc/internal/syscall/syscall_linux.go b/vendor/google.golang.org/grpc/internal/syscall/syscall_linux.go
index c50468a0f..3743bd45a 100644
--- a/vendor/google.golang.org/grpc/internal/syscall/syscall_linux.go
+++ b/vendor/google.golang.org/grpc/internal/syscall/syscall_linux.go
@@ -1,5 +1,3 @@
-// +build !appengine
-
 /*
  *
  * Copyright 2018 gRPC authors.
@@ -43,7 +41,7 @@ func GetCPUTime() int64 {
 	return ts.Nano()
 }
 
-// Rusage is an alias for syscall.Rusage under linux non-appengine environment.
+// Rusage is an alias for syscall.Rusage under linux environment.
 type Rusage syscall.Rusage
 
 // GetRusage returns the resource usage of current process.
diff --git a/vendor/google.golang.org/grpc/internal/syscall/syscall_nonlinux.go b/vendor/google.golang.org/grpc/internal/syscall/syscall_nonlinux.go
index adae60d65..51159e638 100644
--- a/vendor/google.golang.org/grpc/internal/syscall/syscall_nonlinux.go
+++ b/vendor/google.golang.org/grpc/internal/syscall/syscall_nonlinux.go
@@ -35,40 +35,40 @@ var logger = grpclog.Component("core")
 
 func log() {
 	once.Do(func() {
-		logger.Info("CPU time info is unavailable on non-linux or appengine environment.")
+		logger.Info("CPU time info is unavailable on non-linux environment.")
 	})
 }
 
 // GetCPUTime returns the how much CPU time has passed since the start of this process.
-// It always returns 0 under non-linux or appengine environment.
+// It always returns 0 under non-linux environment.
 func GetCPUTime() int64 {
 	log()
 	return 0
 }
 
-// Rusage is an empty struct under non-linux or appengine environment.
+// Rusage is an empty struct under non-linux environment.
 type Rusage struct{}
 
-// GetRusage is a no-op function under non-linux or appengine environment.
+// GetRusage is a no-op function under non-linux environment.
 func GetRusage() (rusage *Rusage) {
 	log()
 	return nil
 }
 
 // CPUTimeDiff returns the differences of user CPU time and system CPU time used
-// between two Rusage structs. It a no-op function for non-linux or appengine environment.
+// between two Rusage structs. It a no-op function for non-linux environment.
 func CPUTimeDiff(first *Rusage, latest *Rusage) (float64, float64) {
 	log()
 	return 0, 0
 }
 
-// SetTCPUserTimeout is a no-op function under non-linux or appengine environments
+// SetTCPUserTimeout is a no-op function under non-linux environments
 func SetTCPUserTimeout(conn net.Conn, timeout time.Duration) error {
 	log()
 	return nil
 }
 
-// GetTCPUserTimeout is a no-op function under non-linux or appengine environments
+// GetTCPUserTimeout is a no-op function under non-linux environments
 // a negative return value indicates the operation is not supported
 func GetTCPUserTimeout(conn net.Conn) (int, error) {
 	log()
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 04cbedf79..3be22fee4 100644
--- a/vendor/google.golang.org/grpc/internal/transport/http2_server.go
+++ b/vendor/google.golang.org/grpc/internal/transport/http2_server.go
@@ -611,6 +611,10 @@ func (t *http2Server) handleData(f *http2.DataFrame) {
 	if !ok {
 		return
 	}
+	if s.getState() == streamReadDone {
+		t.closeStream(s, true, http2.ErrCodeStreamClosed, false)
+		return
+	}
 	if size > 0 {
 		if err := s.fc.onData(size); err != nil {
 			t.closeStream(s, true, http2.ErrCodeFlowControl, false)
diff --git a/vendor/google.golang.org/grpc/regenerate.sh b/vendor/google.golang.org/grpc/regenerate.sh
index 987bc2025..750e3500e 100644
--- a/vendor/google.golang.org/grpc/regenerate.sh
+++ b/vendor/google.golang.org/grpc/regenerate.sh
@@ -39,9 +39,28 @@ mkdir -p ${WORKDIR}/googleapis/google/rpc
 echo "curl https://raw.githubusercontent.com/googleapis/googleapis/master/google/rpc/code.proto"
 curl --silent https://raw.githubusercontent.com/googleapis/googleapis/master/google/rpc/code.proto > ${WORKDIR}/googleapis/google/rpc/code.proto
 
+# Pull in the following repos to build the MeshCA config proto.
+ENVOY_API_REPOS=(
+  "https://github.com/envoyproxy/data-plane-api"
+  "https://github.com/cncf/udpa"
+  "https://github.com/envoyproxy/protoc-gen-validate"
+)
+for repo in ${ENVOY_API_REPOS[@]}; do
+  dirname=$(basename ${repo})
+  mkdir -p ${WORKDIR}/${dirname}
+  echo "git clone ${repo}"
+  git clone --quiet ${repo} ${WORKDIR}/${dirname}
+done
+
+# Pull in the MeshCA service proto.
+mkdir -p ${WORKDIR}/istio/istio/google/security/meshca/v1
+echo "curl https://raw.githubusercontent.com/istio/istio/master/security/proto/providers/google/meshca.proto"
+curl --silent https://raw.githubusercontent.com/istio/istio/master/security/proto/providers/google/meshca.proto > ${WORKDIR}/istio/istio/google/security/meshca/v1/meshca.proto
+
 mkdir -p ${WORKDIR}/out
 
-SOURCES=(
+# Generates legacy gRPC Server symbols in addition to the newer Service symbols
+LEGACY_SOURCES=(
   ${WORKDIR}/googleapis/google/rpc/code.proto
   ${WORKDIR}/grpc-proto/grpc/binlog/v1/binarylog.proto
   ${WORKDIR}/grpc-proto/grpc/channelz/v1/channelz.proto
@@ -51,16 +70,40 @@ SOURCES=(
   ${WORKDIR}/grpc-proto/grpc/health/v1/health.proto
   ${WORKDIR}/grpc-proto/grpc/lb/v1/load_balancer.proto
   ${WORKDIR}/grpc-proto/grpc/lookup/v1/rls.proto
+  ${WORKDIR}/grpc-proto/grpc/lookup/v1/rls_config.proto
   ${WORKDIR}/grpc-proto/grpc/service_config/service_config.proto
-  $(git ls-files --exclude-standard --cached --others "*.proto")
+  ${WORKDIR}/grpc-proto/grpc/tls/provider/meshca/experimental/config.proto
+  ${WORKDIR}/istio/istio/google/security/meshca/v1/meshca.proto
+  profiling/proto/service.proto
+  reflection/grpc_reflection_v1alpha/reflection.proto
+)
+
+# Generates only the new gRPC Service symbols
+SOURCES=(
+  $(git ls-files --exclude-standard --cached --others "*.proto" | grep -v '^\(profiling/proto/service.proto\|reflection/grpc_reflection_v1alpha/reflection.proto\)$')
 )
-OPTS=Mgrpc/service_config/service_config.proto=/internal/proto/grpc_service_config
+
+# These options of the form 'Mfoo.proto=bar' instruct the codegen to use an
+# import path of 'bar' in the generated code when 'foo.proto' is imported in
+# one of the sources.
+OPTS=Mgrpc/service_config/service_config.proto=/internal/proto/grpc_service_config,\
+Menvoy/config/core/v3/config_source.proto=github.com/envoyproxy/go-control-plane/envoy/config/core/v3
+
 for src in ${SOURCES[@]}; do
   echo "protoc ${src}"
-  protoc --go_out=${OPTS}:${WORKDIR}/out --go-grpc_out=${OPTS},requireUnimplementedServers=false:${WORKDIR}/out \
+  protoc --go_out=${OPTS}:${WORKDIR}/out --go-grpc_out=${OPTS}:${WORKDIR}/out ${src}
+done
+
+for src in ${LEGACY_SOURCES[@]}; do
+  echo "protoc ${src}"
+  protoc --go_out=${OPTS},plugins=grpc:${WORKDIR}/out --go-grpc_out=${OPTS},migration_mode=true:${WORKDIR}/out \
     -I"." \
     -I${WORKDIR}/grpc-proto \
     -I${WORKDIR}/googleapis \
+    -I${WORKDIR}/data-plane-api \
+    -I${WORKDIR}/udpa \
+    -I${WORKDIR}/protoc-gen-validate \
+    -I${WORKDIR}/istio \
     ${src}
 done
 
@@ -74,6 +117,10 @@ mv ${WORKDIR}/out/google.golang.org/grpc/lookup/grpc_lookup_v1/* ${WORKDIR}/out/
 rm ${WORKDIR}/out/google.golang.org/grpc/reflection/grpc_testingv3/*.pb.go
 
 # grpc/service_config/service_config.proto does not have a go_package option.
-cp ${WORKDIR}/out/grpc/service_config/service_config.pb.go internal/proto/grpc_service_config
+mv ${WORKDIR}/out/grpc/service_config/service_config.pb.go internal/proto/grpc_service_config
+
+# istio/google/security/meshca/v1/meshca.proto does not have a go_package option.
+mkdir -p ${WORKDIR}/out/google.golang.org/grpc/credentials/tls/certprovider/meshca/internal/v1/
+mv ${WORKDIR}/out/istio/google/security/meshca/v1/* ${WORKDIR}/out/google.golang.org/grpc/credentials/tls/certprovider/meshca/internal/v1/
 
 cp -R ${WORKDIR}/out/google.golang.org/grpc/* .
diff --git a/vendor/google.golang.org/grpc/rpc_util.go b/vendor/google.golang.org/grpc/rpc_util.go
index 8644b8a7d..d4870ba4a 100644
--- a/vendor/google.golang.org/grpc/rpc_util.go
+++ b/vendor/google.golang.org/grpc/rpc_util.go
@@ -866,7 +866,7 @@ type channelzData struct {
 
 // The SupportPackageIsVersion variables are referenced from generated protocol
 // buffer files to ensure compatibility with the gRPC version used.  The latest
-// support package version is 6.
+// support package version is 7.
 //
 // Older versions are kept for compatibility. They may be removed if
 // compatibility cannot be maintained.
@@ -877,6 +877,7 @@ const (
 	SupportPackageIsVersion4 = true
 	SupportPackageIsVersion5 = true
 	SupportPackageIsVersion6 = true
+	SupportPackageIsVersion7 = true
 )
 
 const grpcUA = "grpc-go/" + Version
diff --git a/vendor/google.golang.org/grpc/server.go b/vendor/google.golang.org/grpc/server.go
index 1b56cc2d1..33fc3240b 100644
--- a/vendor/google.golang.org/grpc/server.go
+++ b/vendor/google.golang.org/grpc/server.go
@@ -80,13 +80,14 @@ type ServiceDesc struct {
 	Metadata    interface{}
 }
 
-// service consists of the information of the server serving this service and
-// the methods in this service.
-type service struct {
-	server interface{} // the server for service methods
-	md     map[string]*MethodDesc
-	sd     map[string]*StreamDesc
-	mdata  interface{}
+// serviceInfo wraps information about a service. It is very similar to
+// ServiceDesc and is constructed from it for internal purposes.
+type serviceInfo struct {
+	// Contains the implementation for the methods in this service.
+	serviceImpl interface{}
+	methods     map[string]*MethodDesc
+	streams     map[string]*StreamDesc
+	mdata       interface{}
 }
 
 type serverWorkerData struct {
@@ -99,14 +100,14 @@ type serverWorkerData struct {
 type Server struct {
 	opts serverOptions
 
-	mu     sync.Mutex // guards following
-	lis    map[net.Listener]bool
-	conns  map[transport.ServerTransport]bool
-	serve  bool
-	drain  bool
-	cv     *sync.Cond          // signaled when connections close for GracefulStop
-	m      map[string]*service // service name -> service info
-	events trace.EventLog
+	mu       sync.Mutex // guards following
+	lis      map[net.Listener]bool
+	conns    map[transport.ServerTransport]bool
+	serve    bool
+	drain    bool
+	cv       *sync.Cond              // signaled when connections close for GracefulStop
+	services map[string]*serviceInfo // service name -> service info
+	events   trace.EventLog
 
 	quit               *grpcsync.Event
 	done               *grpcsync.Event
@@ -497,13 +498,13 @@ func NewServer(opt ...ServerOption) *Server {
 		o.apply(&opts)
 	}
 	s := &Server{
-		lis:    make(map[net.Listener]bool),
-		opts:   opts,
-		conns:  make(map[transport.ServerTransport]bool),
-		m:      make(map[string]*service),
-		quit:   grpcsync.NewEvent(),
-		done:   grpcsync.NewEvent(),
-		czData: new(channelzData),
+		lis:      make(map[net.Listener]bool),
+		opts:     opts,
+		conns:    make(map[transport.ServerTransport]bool),
+		services: make(map[string]*serviceInfo),
+		quit:     grpcsync.NewEvent(),
+		done:     grpcsync.NewEvent(),
+		czData:   new(channelzData),
 	}
 	chainUnaryServerInterceptors(s)
 	chainStreamServerInterceptors(s)
@@ -539,14 +540,29 @@ func (s *Server) errorf(format string, a ...interface{}) {
 	}
 }
 
+// ServiceRegistrar wraps a single method that supports service registration. It
+// enables users to pass concrete types other than grpc.Server to the service
+// registration methods exported by the IDL generated code.
+type ServiceRegistrar interface {
+	// RegisterService registers a service and its implementation to the
+	// concrete type implementing this interface.  It may not be called
+	// once the server has started serving.
+	// desc describes the service and its methods and handlers. impl is the
+	// service implementation which is passed to the method handlers.
+	RegisterService(desc *ServiceDesc, impl interface{})
+}
+
 // RegisterService registers a service and its implementation to the gRPC
 // server. It is called from the IDL generated code. This must be called before
-// invoking Serve.
+// invoking Serve. If ss is non-nil (for legacy code), its type is checked to
+// ensure it implements sd.HandlerType.
 func (s *Server) RegisterService(sd *ServiceDesc, ss interface{}) {
-	ht := reflect.TypeOf(sd.HandlerType).Elem()
-	st := reflect.TypeOf(ss)
-	if !st.Implements(ht) {
-		logger.Fatalf("grpc: Server.RegisterService found the handler of type %v that does not satisfy %v", st, ht)
+	if ss != nil {
+		ht := reflect.TypeOf(sd.HandlerType).Elem()
+		st := reflect.TypeOf(ss)
+		if !st.Implements(ht) {
+			logger.Fatalf("grpc: Server.RegisterService found the handler of type %v that does not satisfy %v", st, ht)
+		}
 	}
 	s.register(sd, ss)
 }
@@ -558,24 +574,24 @@ func (s *Server) register(sd *ServiceDesc, ss interface{}) {
 	if s.serve {
 		logger.Fatalf("grpc: Server.RegisterService after Server.Serve for %q", sd.ServiceName)
 	}
-	if _, ok := s.m[sd.ServiceName]; ok {
+	if _, ok := s.services[sd.ServiceName]; ok {
 		logger.Fatalf("grpc: Server.RegisterService found duplicate service registration for %q", sd.ServiceName)
 	}
-	srv := &service{
-		server: ss,
-		md:     make(map[string]*MethodDesc),
-		sd:     make(map[string]*StreamDesc),
-		mdata:  sd.Metadata,
+	info := &serviceInfo{
+		serviceImpl: ss,
+		methods:     make(map[string]*MethodDesc),
+		streams:     make(map[string]*StreamDesc),
+		mdata:       sd.Metadata,
 	}
 	for i := range sd.Methods {
 		d := &sd.Methods[i]
-		srv.md[d.MethodName] = d
+		info.methods[d.MethodName] = d
 	}
 	for i := range sd.Streams {
 		d := &sd.Streams[i]
-		srv.sd[d.StreamName] = d
+		info.streams[d.StreamName] = d
 	}
-	s.m[sd.ServiceName] = srv
+	s.services[sd.ServiceName] = info
 }
 
 // MethodInfo contains the information of an RPC including its method name and type.
@@ -599,16 +615,16 @@ type ServiceInfo struct {
 // Service names include the package names, in the form of <package>.<service>.
 func (s *Server) GetServiceInfo() map[string]ServiceInfo {
 	ret := make(map[string]ServiceInfo)
-	for n, srv := range s.m {
-		methods := make([]MethodInfo, 0, len(srv.md)+len(srv.sd))
-		for m := range srv.md {
+	for n, srv := range s.services {
+		methods := make([]MethodInfo, 0, len(srv.methods)+len(srv.streams))
+		for m := range srv.methods {
 			methods = append(methods, MethodInfo{
 				Name:           m,
 				IsClientStream: false,
 				IsServerStream: false,
 			})
 		}
-		for m, d := range srv.sd {
+		for m, d := range srv.streams {
 			methods = append(methods, MethodInfo{
 				Name:           m,
 				IsClientStream: d.ClientStreams,
@@ -1020,7 +1036,7 @@ func getChainUnaryHandler(interceptors []UnaryServerInterceptor, curr int, info
 	}
 }
 
-func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.Stream, srv *service, md *MethodDesc, trInfo *traceInfo) (err error) {
+func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.Stream, info *serviceInfo, md *MethodDesc, trInfo *traceInfo) (err error) {
 	sh := s.opts.statsHandler
 	if sh != nil || trInfo != nil || channelz.IsOn() {
 		if channelz.IsOn() {
@@ -1143,10 +1159,8 @@ func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.
 	}
 	d, err := recvAndDecompress(&parser{r: stream}, stream, dc, s.opts.maxReceiveMessageSize, payInfo, decomp)
 	if err != nil {
-		if st, ok := status.FromError(err); ok {
-			if e := t.WriteStatus(stream, st); e != nil {
-				channelz.Warningf(logger, s.channelzID, "grpc: Server.processUnaryRPC failed to write status %v", e)
-			}
+		if e := t.WriteStatus(stream, status.Convert(err)); e != nil {
+			channelz.Warningf(logger, s.channelzID, "grpc: Server.processUnaryRPC failed to write status %v", e)
 		}
 		return err
 	}
@@ -1177,7 +1191,7 @@ func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.
 		return nil
 	}
 	ctx := NewContextWithServerTransportStream(stream.Context(), stream)
-	reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt)
+	reply, appErr := md.Handler(info.serviceImpl, ctx, df, s.opts.unaryInt)
 	if appErr != nil {
 		appStatus, ok := status.FromError(appErr)
 		if !ok {
@@ -1303,7 +1317,7 @@ func getChainStreamHandler(interceptors []StreamServerInterceptor, curr int, inf
 	}
 }
 
-func (s *Server) processStreamingRPC(t transport.ServerTransport, stream *transport.Stream, srv *service, sd *StreamDesc, trInfo *traceInfo) (err error) {
+func (s *Server) processStreamingRPC(t transport.ServerTransport, stream *transport.Stream, info *serviceInfo, sd *StreamDesc, trInfo *traceInfo) (err error) {
 	if channelz.IsOn() {
 		s.incrCallsStarted()
 	}
@@ -1420,8 +1434,8 @@ func (s *Server) processStreamingRPC(t transport.ServerTransport, stream *transp
 	}
 	var appErr error
 	var server interface{}
-	if srv != nil {
-		server = srv.server
+	if info != nil {
+		server = info.serviceImpl
 	}
 	if s.opts.streamInt == nil {
 		appErr = sd.Handler(server, ss)
@@ -1497,13 +1511,13 @@ func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Str
 	service := sm[:pos]
 	method := sm[pos+1:]
 
-	srv, knownService := s.m[service]
+	srv, knownService := s.services[service]
 	if knownService {
-		if md, ok := srv.md[method]; ok {
+		if md, ok := srv.methods[method]; ok {
 			s.processUnaryRPC(t, stream, srv, md, trInfo)
 			return
 		}
-		if sd, ok := srv.sd[method]; ok {
+		if sd, ok := srv.streams[method]; ok {
 			s.processStreamingRPC(t, stream, srv, sd, trInfo)
 			return
 		}
diff --git a/vendor/google.golang.org/grpc/version.go b/vendor/google.golang.org/grpc/version.go
index de387f7b8..89d37ce84 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.31.1"
+const Version = "1.32.0"
diff --git a/vendor/google.golang.org/grpc/vet.sh b/vendor/google.golang.org/grpc/vet.sh
index 8b7dff19a..c52c1bdbf 100644
--- a/vendor/google.golang.org/grpc/vet.sh
+++ b/vendor/google.golang.org/grpc/vet.sh
@@ -83,20 +83,19 @@ not git grep -l 'x/net/context' -- "*.go"
 #   thread safety.
 git grep -l '"math/rand"' -- "*.go" 2>&1 | not grep -v '^examples\|^stress\|grpcrand\|^benchmark\|wrr_test'
 
+# - Do not call grpclog directly. Use grpclog.Component instead.
+git grep -l 'grpclog.I\|grpclog.W\|grpclog.E\|grpclog.F\|grpclog.V' -- "*.go" | not grep -v '^grpclog/component.go\|^internal/grpctest/tlogger_test.go'
+
 # - Ensure all ptypes proto packages are renamed when importing.
 not git grep "\(import \|^\s*\)\"github.com/golang/protobuf/ptypes/" -- "*.go"
 
 # - Ensure all xds proto imports are renamed to *pb or *grpc.
-git grep '"github.com/envoyproxy/go-control-plane/envoy' -- '*.go' | not grep -v 'pb "\|grpc "'
-
-# - Check imports that are illegal in appengine (until Go 1.11).
-# TODO: Remove when we drop Go 1.10 support
-go list -f {{.Dir}} ./... | xargs go run test/go_vet/vet.go
+git grep '"github.com/envoyproxy/go-control-plane/envoy' -- '*.go' ':(exclude)*.pb.go' | not grep -v 'pb "\|grpc "'
 
 # - gofmt, goimports, golint (with exceptions for generated code), go vet.
 gofmt -s -d -l . 2>&1 | fail_on_output
-goimports -l . 2>&1 | not grep -vE "(_mock|\.pb)\.go"
-golint ./... 2>&1 | not grep -vE "(_mock|\.pb)\.go:"
+goimports -l . 2>&1 | not grep -vE "\.pb\.go"
+golint ./... 2>&1 | not grep -vE "\.pb\.go:"
 go vet -all ./...
 
 misspell -error .
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0951752a0..e1fc822b4 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -19,7 +19,7 @@ github.com/VictoriaMetrics/metrics
 # github.com/VictoriaMetrics/metricsql v0.5.1
 github.com/VictoriaMetrics/metricsql
 github.com/VictoriaMetrics/metricsql/binaryop
-# github.com/aws/aws-sdk-go v1.34.18
+# github.com/aws/aws-sdk-go v1.34.20
 github.com/aws/aws-sdk-go/aws
 github.com/aws/aws-sdk-go/aws/arn
 github.com/aws/aws-sdk-go/aws/awserr
@@ -149,7 +149,7 @@ golang.org/x/oauth2/google
 golang.org/x/oauth2/internal
 golang.org/x/oauth2/jws
 golang.org/x/oauth2/jwt
-# golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f
+# golang.org/x/sys v0.0.0-20200909081042-eff7692f9009
 golang.org/x/sys/internal/unsafeheader
 golang.org/x/sys/unix
 # golang.org/x/text v0.3.3
@@ -157,7 +157,7 @@ golang.org/x/text/secure/bidirule
 golang.org/x/text/transform
 golang.org/x/text/unicode/bidi
 golang.org/x/text/unicode/norm
-# golang.org/x/tools v0.0.0-20200904185747-39188db58858
+# golang.org/x/tools v0.0.0-20200909210914-44a2922940c2
 golang.org/x/tools/cmd/goimports
 golang.org/x/tools/go/ast/astutil
 golang.org/x/tools/go/gcexportdata
@@ -203,7 +203,7 @@ google.golang.org/genproto/googleapis/iam/v1
 google.golang.org/genproto/googleapis/rpc/code
 google.golang.org/genproto/googleapis/rpc/status
 google.golang.org/genproto/googleapis/type/expr
-# google.golang.org/grpc v1.31.1
+# google.golang.org/grpc v1.32.0
 google.golang.org/grpc
 google.golang.org/grpc/attributes
 google.golang.org/grpc/backoff
@@ -215,7 +215,6 @@ google.golang.org/grpc/binarylog/grpc_binarylog_v1
 google.golang.org/grpc/codes
 google.golang.org/grpc/connectivity
 google.golang.org/grpc/credentials
-google.golang.org/grpc/credentials/internal
 google.golang.org/grpc/encoding
 google.golang.org/grpc/encoding/proto
 google.golang.org/grpc/grpclog


Test patch:
diff --git a/app/vmalert/config/config_test.go b/app/vmalert/config/config_test.go
index d46b4ba23..d01665980 100644
--- a/app/vmalert/config/config_test.go
+++ b/app/vmalert/config/config_test.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
+	"gopkg.in/yaml.v2"
 )
 
 func TestMain(m *testing.M) {
@@ -320,3 +321,36 @@ func TestHashRule(t *testing.T) {
 		}
 	}
 }
+
+func TestGroupChecksum(t *testing.T) {
+	data := `
+name: TestGroup
+rules:
+  - alert: ExampleAlertAlwaysFiring
+    expr: sum by(job) (up == 1)
+  - record: handler:requests:rate5m
+    expr: sum(rate(prometheus_http_requests_total[5m])) by (handler)
+`
+	var g Group
+	if err := yaml.Unmarshal([]byte(data), &g); err != nil {
+		t.Fatalf("failed to unmarshal: %s", err)
+	}
+	if g.Checksum == "" {
+		t.Fatalf("expected to get non-empty checksum")
+	}
+	newData := `
+name: TestGroup
+rules:
+  - record: handler:requests:rate5m
+    expr: sum(rate(prometheus_http_requests_total[5m])) by (handler)
+  - alert: ExampleAlertAlwaysFiring
+    expr: sum by(job) (up == 1)
+`
+	var ng Group
+	if err := yaml.Unmarshal([]byte(newData), &g); err != nil {
+		t.Fatalf("failed to unmarshal: %s", err)
+	}
+	if g.Checksum == ng.Checksum {
+		t.Fatalf("expected to get different checksums")
+	}
+}
diff --git a/app/vmselect/graphite/graphite_test.go b/app/vmselect/graphite/graphite_test.go
new file mode 100644
index 000000000..89d9b0948
--- /dev/null
+++ b/app/vmselect/graphite/graphite_test.go
@@ -0,0 +1,71 @@
+package graphite
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestGetRegexpForQuery(t *testing.T) {
+	f := func(query string, delimiter byte, reExpected string) {
+		t.Helper()
+		re, err := getRegexpForQuery(query, delimiter)
+		if err != nil {
+			t.Fatalf("unexpected error in getRegexpForQuery(%q): %s", query, err)
+		}
+		reStr := re.String()
+		if reStr != reExpected {
+			t.Fatalf("unexpected regexp for query=%q, delimiter=%c; got %s; want %s", query, delimiter, reStr, reExpected)
+		}
+	}
+	f("", '.', "")
+	f("foobar", '.', "foobar")
+	f("*", '.', `[^\.]*`)
+	f("*", '_', `[^_]*`)
+	f("foo.*.bar", '.', `foo\.[^\.]*\.bar`)
+	f("fo*b{ar,aaa}[a-z]xx*.d", '.', `fo[^\.]*b(?:ar|aaa)[a-z]xx[^\.]*\.d`)
+	f("fo*b{ar,aaa}[a-z]xx*_d", '_', `fo[^_]*b(?:ar|aaa)[a-z]xx[^_]*_d`)
+}
+
+func TestSortPaths(t *testing.T) {
+	f := func(paths []string, delimiter string, pathsSortedExpected []string) {
+		t.Helper()
+		sortPaths(paths, delimiter)
+		if !reflect.DeepEqual(paths, pathsSortedExpected) {
+			t.Fatalf("unexpected sortPaths result;\ngot\n%q\nwant\n%q", paths, pathsSortedExpected)
+		}
+	}
+	f([]string{"foo", "bar"}, ".", []string{"bar", "foo"})
+	f([]string{"foo.", "bar", "aa", "ab."}, ".", []string{"ab.", "foo.", "aa", "bar"})
+	f([]string{"foo.", "bar", "aa", "ab."}, "_", []string{"aa", "ab.", "bar", "foo."})
+}
+
+func TestFilterLeaves(t *testing.T) {
+	f := func(paths []string, delimiter string, leavesExpected []string) {
+		t.Helper()
+		leaves := filterLeaves(paths, delimiter)
+		if !reflect.DeepEqual(leaves, leavesExpected) {
+			t.Fatalf("unexpected leaves; got\n%q\nwant\n%q", leaves, leavesExpected)
+		}
+	}
+	f([]string{"foo", "bar"}, ".", []string{"foo", "bar"})
+	f([]string{"a.", ".", "bc"}, ".", []string{"bc"})
+	f([]string{"a.", ".", "bc"}, "_", []string{"a.", ".", "bc"})
+	f([]string{"a_", "_", "bc"}, "_", []string{"bc"})
+	f([]string{"foo.", "bar."}, ".", []string{})
+}
+
+func TestAddAutomaticVariants(t *testing.T) {
+	f := func(query, delimiter, resultExpected string) {
+		t.Helper()
+		result := addAutomaticVariants(query, delimiter)
+		if result != resultExpected {
+			t.Fatalf("unexpected result for addAutomaticVariants(%q, delimiter=%q); got %q; want %q", query, delimiter, result, resultExpected)
+		}
+	}
+	f("", ".", "")
+	f("foobar", ".", "foobar")
+	f("foo,bar.baz", ".", "{foo,bar}.baz")
+	f("foo,bar.baz", "_", "{foo,bar.baz}")
+	f("foo,bar_baz*", "_", "{foo,bar}_baz*")
+	f("foo.bar,baz,aa.bb,cc", ".", "foo.{bar,baz,aa}.{bb,cc}")
+}
diff --git a/app/vmselect/prometheus/prometheus_test.go b/app/vmselect/prometheus/prometheus_test.go
index c86734fb0..70d2b06f2 100644
--- a/app/vmselect/prometheus/prometheus_test.go
+++ b/app/vmselect/prometheus/prometheus_test.go
@@ -1,10 +1,7 @@
 package prometheus
 
 import (
-	"fmt"
 	"math"
-	"net/http"
-	"net/url"
 	"reflect"
 	"testing"
 
@@ -50,76 +47,6 @@ func TestRemoveEmptyValuesAndTimeseries(t *testing.T) {
 	})
 }
 
-func TestGetTimeSuccess(t *testing.T) {
-	f := func(s string, timestampExpected int64) {
-		t.Helper()
-		urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
-		r, err := http.NewRequest("GET", urlStr, nil)
-		if err != nil {
-			t.Fatalf("unexpected error in NewRequest: %s", err)
-		}
-
-		// Verify defaultValue
-		ts, err := getTime(r, "foo", 123)
-		if err != nil {
-			t.Fatalf("unexpected error when obtaining default time from getTime(%q): %s", s, err)
-		}
-		if ts != 123 {
-			t.Fatalf("unexpected default value for getTime(%q); got %d; want %d", s, ts, 123)
-		}
-
-		// Verify timestampExpected
-		ts, err = getTime(r, "s", 123)
-		if err != nil {
-			t.Fatalf("unexpected error in getTime(%q): %s", s, err)
-		}
-		if ts != timestampExpected {
-			t.Fatalf("unexpected timestamp for getTime(%q); got %d; want %d", s, ts, timestampExpected)
-		}
-	}
-
-	f("2019-07-07T20:01:02Z", 1562529662000)
-	f("2019-07-07T20:47:40+03:00", 1562521660000)
-	f("-292273086-05-16T16:47:06Z", minTimeMsecs)
-	f("292277025-08-18T07:12:54.999999999Z", maxTimeMsecs)
-	f("1562529662.324", 1562529662324)
-	f("-9223372036.854", minTimeMsecs)
-	f("-9223372036.855", minTimeMsecs)
-	f("9223372036.855", maxTimeMsecs)
-}
-
-func TestGetTimeError(t *testing.T) {
-	f := func(s string) {
-		t.Helper()
-		urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
-		r, err := http.NewRequest("GET", urlStr, nil)
-		if err != nil {
-			t.Fatalf("unexpected error in NewRequest: %s", err)
-		}
-
-		// Verify defaultValue
-		ts, err := getTime(r, "foo", 123)
-		if err != nil {
-			t.Fatalf("unexpected error when obtaining default time from getTime(%q): %s", s, err)
-		}
-		if ts != 123 {
-			t.Fatalf("unexpected default value for getTime(%q); got %d; want %d", s, ts, 123)
-		}
-
-		// Verify timestampExpected
-		_, err = getTime(r, "s", 123)
-		if err == nil {
-			t.Fatalf("expecting non-nil error in getTime(%q)", s)
-		}
-	}
-
-	f("foo")
-	f("2019-07-07T20:01:02Zisdf")
-	f("2019-07-07T20:47:40+03:00123")
-	f("-292273086-05-16T16:47:07Z")
-	f("292277025-08-18T07:12:54.999999998Z")
-}
-
 func TestAdjustLastPoints(t *testing.T) {
 	f := func(tss []netstorage.Result, start, end int64, tssExpected []netstorage.Result) {
 		t.Helper()
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index 48a276f23..6f2b1d660 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
 )
 
@@ -21,7 +22,7 @@ func TestExecSuccess(t *testing.T) {
 			Start:    start,
 			End:      end,
 			Step:     step,
-			Deadline: netstorage.NewDeadline(time.Now(), time.Minute, ""),
+			Deadline: searchutils.NewDeadline(time.Now(), time.Minute, ""),
 		}
 		for i := 0; i < 5; i++ {
 			result, err := Exec(ec, q, false)
@@ -5896,7 +5897,7 @@ func TestExecError(t *testing.T) {
 			Start:    1000,
 			End:      2000,
 			Step:     100,
-			Deadline: netstorage.NewDeadline(time.Now(), time.Minute, ""),
+			Deadline: searchutils.NewDeadline(time.Now(), time.Minute, ""),
 		}
 		for i := 0; i < 4; i++ {
 			rv, err := Exec(ec, q, false)
diff --git a/app/vmselect/searchutils/searchutils_test.go b/app/vmselect/searchutils/searchutils_test.go
new file mode 100644
index 000000000..c5d6c7949
--- /dev/null
+++ b/app/vmselect/searchutils/searchutils_test.go
@@ -0,0 +1,78 @@
+package searchutils
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func TestGetTimeSuccess(t *testing.T) {
+	f := func(s string, timestampExpected int64) {
+		t.Helper()
+		urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
+		r, err := http.NewRequest("GET", urlStr, nil)
+		if err != nil {
+			t.Fatalf("unexpected error in NewRequest: %s", err)
+		}
+
+		// Verify defaultValue
+		ts, err := GetTime(r, "foo", 123)
+		if err != nil {
+			t.Fatalf("unexpected error when obtaining default time from GetTime(%q): %s", s, err)
+		}
+		if ts != 123 {
+			t.Fatalf("unexpected default value for GetTime(%q); got %d; want %d", s, ts, 123)
+		}
+
+		// Verify timestampExpected
+		ts, err = GetTime(r, "s", 123)
+		if err != nil {
+			t.Fatalf("unexpected error in GetTime(%q): %s", s, err)
+		}
+		if ts != timestampExpected {
+			t.Fatalf("unexpected timestamp for GetTime(%q); got %d; want %d", s, ts, timestampExpected)
+		}
+	}
+
+	f("2019-07-07T20:01:02Z", 1562529662000)
+	f("2019-07-07T20:47:40+03:00", 1562521660000)
+	f("-292273086-05-16T16:47:06Z", minTimeMsecs)
+	f("292277025-08-18T07:12:54.999999999Z", maxTimeMsecs)
+	f("1562529662.324", 1562529662324)
+	f("-9223372036.854", minTimeMsecs)
+	f("-9223372036.855", minTimeMsecs)
+	f("9223372036.855", maxTimeMsecs)
+}
+
+func TestGetTimeError(t *testing.T) {
+	f := func(s string) {
+		t.Helper()
+		urlStr := fmt.Sprintf("http://foo.bar/baz?s=%s", url.QueryEscape(s))
+		r, err := http.NewRequest("GET", urlStr, nil)
+		if err != nil {
+			t.Fatalf("unexpected error in NewRequest: %s", err)
+		}
+
+		// Verify defaultValue
+		ts, err := GetTime(r, "foo", 123)
+		if err != nil {
+			t.Fatalf("unexpected error when obtaining default time from GetTime(%q): %s", s, err)
+		}
+		if ts != 123 {
+			t.Fatalf("unexpected default value for GetTime(%q); got %d; want %d", s, ts, 123)
+		}
+
+		// Verify timestampExpected
+		_, err = GetTime(r, "s", 123)
+		if err == nil {
+			t.Fatalf("expecting non-nil error in GetTime(%q)", s)
+		}
+	}
+
+	f("foo")
+	f("2019-07-07T20:01:02Zisdf")
+	f("2019-07-07T20:47:40+03:00123")
+	f("-292273086-05-16T16:47:07Z")
+	f("292277025-08-18T07:12:54.999999998Z")
+}
diff --git a/lib/promscrape/discovery/kubernetes/endpointslices_test.go b/lib/promscrape/discovery/kubernetes/endpointslices_test.go
new file mode 100644
index 000000000..44acef3d7
--- /dev/null
+++ b/lib/promscrape/discovery/kubernetes/endpointslices_test.go
@@ -0,0 +1,446 @@
+package kubernetes
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
+)
+
+func Test_parseEndpointSlicesListFail(t *testing.T) {
+	f := func(data string) {
+		eslList, err := parseEndpointSlicesList([]byte(data))
+		if err == nil {
+			t.Errorf("unexpected result, test must fail! data: %s", data)
+		}
+		if eslList != nil {
+			t.Errorf("endpointSliceList must be nil, got: %v", eslList)
+		}
+	}
+
+	f(``)
+	f(`{"items": [1,2,3]`)
+	f(`{"items": [
+    {
+      "metadata": {
+        "name": "kubernetes"}]}`)
+
+}
+
+func Test_parseEndpointSlicesListSuccess(t *testing.T) {
+	data := `{
+  "kind": "EndpointSliceList",
+  "apiVersion": "discovery.k8s.io/v1beta1",
+  "metadata": {
+    "selfLink": "/apis/discovery.k8s.io/v1beta1/endpointslices",
+    "resourceVersion": "1177"
+  },
+  "items": [
+    {
+      "metadata": {
+        "name": "kubernetes",
+        "namespace": "default",
+        "selfLink": "/apis/discovery.k8s.io/v1beta1/namespaces/default/endpointslices/kubernetes",
+        "uid": "a60d9173-5fe4-4bc3-87a6-269daee71f8a",
+        "resourceVersion": "159",
+        "generation": 1,
+        "creationTimestamp": "2020-09-07T14:27:22Z",
+        "labels": {
+          "kubernetes.io/service-name": "kubernetes"
+        },
+        "managedFields": [
+          {
+            "manager": "kube-apiserver",
+            "operation": "Update",
+            "apiVersion": "discovery.k8s.io/v1beta1",
+            "time": "2020-09-07T14:27:22Z",
+            "fieldsType": "FieldsV1",
+            "fieldsV1": {"f:addressType":{},"f:endpoints":{},"f:metadata":{"f:labels":{".":{},"f:kubernetes.io/service-name":{}}},"f:ports":{}}
+          }
+        ]
+      },
+      "addressType": "IPv4",
+      "endpoints": [
+        {
+          "addresses": [
+            "172.18.0.2"
+          ],
+          "conditions": {
+            "ready": true
+          }
+        }
+      ],
+      "ports": [
+        {
+          "name": "https",
+          "protocol": "TCP",
+          "port": 6443
+        }
+      ]
+    },
+    {
+      "metadata": {
+        "name": "kube-dns-22mvb",
+        "generateName": "kube-dns-",
+        "namespace": "kube-system",
+        "selfLink": "/apis/discovery.k8s.io/v1beta1/namespaces/kube-system/endpointslices/kube-dns-22mvb",
+        "uid": "7c95c854-f34c-48e1-86f5-bb8269113c11",
+        "resourceVersion": "604",
+        "generation": 5,
+        "creationTimestamp": "2020-09-07T14:27:39Z",
+        "labels": {
+          "endpointslice.kubernetes.io/managed-by": "endpointslice-controller.k8s.io",
+          "kubernetes.io/service-name": "kube-dns"
+        },
+        "annotations": {
+          "endpoints.kubernetes.io/last-change-trigger-time": "2020-09-07T14:28:35Z"
+        },
+        "ownerReferences": [
+          {
+            "apiVersion": "v1",
+            "kind": "Service",
+            "name": "kube-dns",
+            "uid": "509e80d8-6d05-487b-bfff-74f5768f1024",
+            "controller": true,
+            "blockOwnerDeletion": true
+          }
+        ],
+        "managedFields": [
+          {
+            "manager": "kube-controller-manager",
+            "operation": "Update",
+            "apiVersion": "discovery.k8s.io/v1beta1",
+            "time": "2020-09-07T14:28:35Z",
+            "fieldsType": "FieldsV1",
+            "fieldsV1": {"f:addressType":{},"f:endpoints":{},"f:metadata":{"f:annotations":{".":{},"f:endpoints.kubernetes.io/last-change-trigger-time":{}},"f:generateName":{},"f:labels":{".":{},"f:endpointslice.kubernetes.io/managed-by":{},"f:kubernetes.io/service-name":{}},"f:ownerReferences":{".":{},"k:{\"uid\":\"509e80d8-6d05-487b-bfff-74f5768f1024\"}":{".":{},"f:apiVersion":{},"f:blockOwnerDeletion":{},"f:controller":{},"f:kind":{},"f:name":{},"f:uid":{}}}},"f:ports":{}}
+          }
+        ]
+      },
+      "addressType": "IPv4",
+      "endpoints": [
+        {
+          "addresses": [
+            "10.244.0.3"
+          ],
+          "conditions": {
+            "ready": true
+          },
+          "targetRef": {
+            "kind": "Pod",
+            "namespace": "kube-system",
+            "name": "coredns-66bff467f8-z8czk",
+            "uid": "36a545ff-dbba-4192-a5f6-1dbb0c21c73d",
+            "resourceVersion": "603"
+          },
+          "topology": {
+            "kubernetes.io/hostname": "kind-control-plane"
+          }
+        },
+        {
+          "addresses": [
+            "10.244.0.4"
+          ],
+          "conditions": {
+            "ready": true
+          },
+          "targetRef": {
+            "kind": "Pod",
+            "namespace": "kube-system",
+            "name": "coredns-66bff467f8-kpbhk",
+            "uid": "db38d8b4-847a-4e82-874c-fe444fba2718",
+            "resourceVersion": "576"
+          },
+          "topology": {
+            "kubernetes.io/hostname": "kind-control-plane"
+          }
+        }
+      ],
+      "ports": [
+        {
+          "name": "dns-tcp",
+          "protocol": "TCP",
+          "port": 53
+        },
+        {
+          "name": "metrics",
+          "protocol": "TCP",
+          "port": 9153
+        },
+        {
+          "name": "dns",
+          "protocol": "UDP",
+          "port": 53
+        }
+      ]
+    }
+  ]
+}`
+	esl, err := parseEndpointSlicesList([]byte(data))
+	if err != nil {
+		t.Errorf("cannot parse data for EndpointSliceList: %v", err)
+		return
+	}
+	if len(esl.Items) != 2 {
+		t.Fatalf("expected 2 items at endpointSliceList, got: %d", len(esl.Items))
+	}
+
+	firstEsl := esl.Items[0]
+	got := firstEsl.appendTargetLabels(nil, nil, nil)
+	sortedLables := [][]prompbmarshal.Label{}
+	for _, labels := range got {
+		sortedLables = append(sortedLables, discoveryutils.GetSortedLabels(labels))
+	}
+	expectedLabels := [][]prompbmarshal.Label{
+		discoveryutils.GetSortedLabels(map[string]string{
+			"__address__": "172.18.0.2:6443",
+			"__meta_kubernetes_endpointslice_address_type":              "IPv4",
+			"__meta_kubernetes_endpointslice_endpoint_conditions_ready": "true",
+			"__meta_kubernetes_endpointslice_name":                      "kubernetes",
+			"__meta_kubernetes_endpointslice_port":                      "6443",
+			"__meta_kubernetes_endpointslice_port_name":                 "https",
+			"__meta_kubernetes_endpointslice_port_protocol":             "TCP",
+			"__meta_kubernetes_namespace":                               "default",
+		})}
+	if !reflect.DeepEqual(sortedLables, expectedLabels) {
+		t.Fatalf("unexpected labels,\ngot:\n%v,\nwant:\n%v", sortedLables, expectedLabels)
+	}
+
+}
+
+func TestEndpointSlice_appendTargetLabels(t *testing.T) {
+	type fields struct {
+		Metadata    ObjectMeta
+		Endpoints   []Endpoint
+		AddressType string
+		Ports       []EndpointPort
+	}
+	type args struct {
+		ms   []map[string]string
+		pods []Pod
+		svcs []Service
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   [][]prompbmarshal.Label
+	}{
+		{
+			name: "simple eps",
+			args: args{},
+			fields: fields{
+				Metadata: ObjectMeta{
+					Name:      "fake-esl",
+					Namespace: "default",
+				},
+				AddressType: "ipv4",
+				Endpoints: []Endpoint{
+					{Addresses: []string{"127.0.0.1"},
+						Hostname:   "node-1",
+						Topology:   map[string]string{"kubernetes.topoligy.io/zone": "gce-1"},
+						Conditions: EndpointConditions{Ready: true},
+						TargetRef: ObjectReference{
+							Kind:      "Pod",
+							Namespace: "default",
+							Name:      "main-pod",
+						},
+					},
+				},
+				Ports: []EndpointPort{
+					{
+						Name:        "http",
+						Port:        8085,
+						AppProtocol: "http",
+						Protocol:    "tcp",
+					},
+				},
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address__": "127.0.0.1:8085",
+					"__meta_kubernetes_endpointslice_address_target_kind":                                   "Pod",
+					"__meta_kubernetes_endpointslice_address_target_name":                                   "main-pod",
+					"__meta_kubernetes_endpointslice_address_type":                                          "ipv4",
+					"__meta_kubernetes_endpointslice_endpoint_conditions_ready":                             "true",
+					"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_topoligy_io_zone":         "gce-1",
+					"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_topoligy_io_zone": "true",
+					"__meta_kubernetes_endpointslice_endpoint_hostname":                                     "node-1",
+					"__meta_kubernetes_endpointslice_name":                                                  "fake-esl",
+					"__meta_kubernetes_endpointslice_port":                                                  "8085",
+					"__meta_kubernetes_endpointslice_port_app_protocol":                                     "http",
+					"__meta_kubernetes_endpointslice_port_name":                                             "http",
+					"__meta_kubernetes_endpointslice_port_protocol":                                         "tcp",
+					"__meta_kubernetes_namespace":                                                           "default",
+				}),
+			},
+		},
+		{
+			name: "eps with pods and services",
+			args: args{
+				pods: []Pod{
+					{
+						Metadata: ObjectMeta{
+							UID:       "some-pod-uuid",
+							Namespace: "monitoring",
+							Name:      "main-pod",
+							Labels: discoveryutils.GetSortedLabels(map[string]string{
+								"pod-label-1": "pod-value-1",
+								"pod-label-2": "pod-value-2",
+							}),
+							Annotations: discoveryutils.GetSortedLabels(map[string]string{
+								"pod-annotations-1": "annotation-value-1",
+							}),
+						},
+						Status: PodStatus{PodIP: "192.168.11.5", HostIP: "172.15.1.1"},
+						Spec: PodSpec{NodeName: "node-2", Containers: []Container{
+							{
+								Name: "container-1",
+								Ports: []ContainerPort{
+									{
+										ContainerPort: 8085,
+										Protocol:      "tcp",
+										Name:          "http",
+									},
+									{
+										ContainerPort: 8011,
+										Protocol:      "udp",
+										Name:          "dns",
+									},
+								},
+							},
+						}},
+					},
+				},
+				svcs: []Service{
+					{
+						Spec: ServiceSpec{Type: "ClusterIP", Ports: []ServicePort{
+							{
+								Name:     "http",
+								Protocol: "tcp",
+								Port:     8085,
+							},
+						}},
+						Metadata: ObjectMeta{
+							Name:      "custom-esl",
+							Namespace: "monitoring",
+							Labels: discoveryutils.GetSortedLabels(map[string]string{
+								"service-label-1": "value-1",
+								"service-label-2": "value-2",
+							}),
+						},
+					},
+				},
+			},
+			fields: fields{
+				Metadata: ObjectMeta{
+					Name:      "custom-esl",
+					Namespace: "monitoring",
+				},
+				AddressType: "ipv4",
+				Endpoints: []Endpoint{
+					{Addresses: []string{"127.0.0.1"},
+						Hostname:   "node-1",
+						Topology:   map[string]string{"kubernetes.topoligy.io/zone": "gce-1"},
+						Conditions: EndpointConditions{Ready: true},
+						TargetRef: ObjectReference{
+							Kind:      "Pod",
+							Namespace: "monitoring",
+							Name:      "main-pod",
+						},
+					},
+				},
+				Ports: []EndpointPort{
+					{
+						Name:        "http",
+						Port:        8085,
+						AppProtocol: "http",
+						Protocol:    "tcp",
+					},
+				},
+			},
+			want: [][]prompbmarshal.Label{
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address__": "127.0.0.1:8085",
+					"__meta_kubernetes_endpointslice_address_target_kind":                                   "Pod",
+					"__meta_kubernetes_endpointslice_address_target_name":                                   "main-pod",
+					"__meta_kubernetes_endpointslice_address_type":                                          "ipv4",
+					"__meta_kubernetes_endpointslice_endpoint_conditions_ready":                             "true",
+					"__meta_kubernetes_endpointslice_endpoint_topology_kubernetes_topoligy_io_zone":         "gce-1",
+					"__meta_kubernetes_endpointslice_endpoint_topology_present_kubernetes_topoligy_io_zone": "true",
+					"__meta_kubernetes_endpointslice_endpoint_hostname":                                     "node-1",
+					"__meta_kubernetes_endpointslice_name":                                                  "custom-esl",
+					"__meta_kubernetes_endpointslice_port":                                                  "8085",
+					"__meta_kubernetes_endpointslice_port_app_protocol":                                     "http",
+					"__meta_kubernetes_endpointslice_port_name":                                             "http",
+					"__meta_kubernetes_endpointslice_port_protocol":                                         "tcp",
+					"__meta_kubernetes_namespace":                                                           "monitoring",
+					"__meta_kubernetes_pod_annotation_pod_annotations_1":                                    "annotation-value-1",
+					"__meta_kubernetes_pod_annotationpresent_pod_annotations_1":                             "true",
+					"__meta_kubernetes_pod_container_name":                                                  "container-1",
+					"__meta_kubernetes_pod_container_port_name":                                             "http",
+					"__meta_kubernetes_pod_container_port_number":                                           "8085",
+					"__meta_kubernetes_pod_container_port_protocol":                                         "tcp",
+					"__meta_kubernetes_pod_host_ip":                                                         "172.15.1.1",
+					"__meta_kubernetes_pod_ip":                                                              "192.168.11.5",
+					"__meta_kubernetes_pod_label_pod_label_1":                                               "pod-value-1",
+					"__meta_kubernetes_pod_label_pod_label_2":                                               "pod-value-2",
+					"__meta_kubernetes_pod_labelpresent_pod_label_1":                                        "true",
+					"__meta_kubernetes_pod_labelpresent_pod_label_2":                                        "true",
+					"__meta_kubernetes_pod_name":                                                            "main-pod",
+					"__meta_kubernetes_pod_node_name":                                                       "node-2",
+					"__meta_kubernetes_pod_phase":                                                           "",
+					"__meta_kubernetes_pod_ready":                                                           "unknown",
+					"__meta_kubernetes_pod_uid":                                                             "some-pod-uuid",
+					"__meta_kubernetes_service_cluster_ip":                                                  "",
+					"__meta_kubernetes_service_label_service_label_1":                                       "value-1",
+					"__meta_kubernetes_service_label_service_label_2":                                       "value-2",
+					"__meta_kubernetes_service_labelpresent_service_label_1":                                "true",
+					"__meta_kubernetes_service_labelpresent_service_label_2":                                "true",
+					"__meta_kubernetes_service_name":                                                        "custom-esl",
+					"__meta_kubernetes_service_type":                                                        "ClusterIP",
+				}),
+				discoveryutils.GetSortedLabels(map[string]string{
+					"__address__":                 "192.168.11.5:8011",
+					"__meta_kubernetes_namespace": "monitoring",
+					"__meta_kubernetes_pod_annotation_pod_annotations_1":        "annotation-value-1",
+					"__meta_kubernetes_pod_annotationpresent_pod_annotations_1": "true",
+					"__meta_kubernetes_pod_container_name":                      "container-1",
+					"__meta_kubernetes_pod_container_port_name":                 "dns",
+					"__meta_kubernetes_pod_container_port_number":               "8011",
+					"__meta_kubernetes_pod_container_port_protocol":             "udp",
+					"__meta_kubernetes_pod_host_ip":                             "172.15.1.1",
+					"__meta_kubernetes_pod_ip":                                  "192.168.11.5",
+					"__meta_kubernetes_pod_label_pod_label_1":                   "pod-value-1",
+					"__meta_kubernetes_pod_label_pod_label_2":                   "pod-value-2",
+					"__meta_kubernetes_pod_labelpresent_pod_label_1":            "true",
+					"__meta_kubernetes_pod_labelpresent_pod_label_2":            "true",
+					"__meta_kubernetes_pod_name":                                "main-pod",
+					"__meta_kubernetes_pod_node_name":                           "node-2",
+					"__meta_kubernetes_pod_phase":                               "",
+					"__meta_kubernetes_pod_ready":                               "unknown",
+					"__meta_kubernetes_pod_uid":                                 "some-pod-uuid",
+				}),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			eps := &EndpointSlice{
+				Metadata:    tt.fields.Metadata,
+				Endpoints:   tt.fields.Endpoints,
+				AddressType: tt.fields.AddressType,
+				Ports:       tt.fields.Ports,
+			}
+			got := eps.appendTargetLabels(tt.args.ms, tt.args.pods, tt.args.svcs)
+			var sortedLabelss [][]prompbmarshal.Label
+			for _, labels := range got {
+				sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))
+			}
+
+			if !reflect.DeepEqual(sortedLabelss, tt.want) {
+				t.Errorf("got unxpected labels: \ngot:\n %v, \nexpect:\n %v", sortedLabelss, tt.want)
+			}
+		})
+	}
+}
diff --git a/lib/protoparser/prometheus/streamparser_test.go b/lib/protoparser/prometheus/streamparser_test.go
index 3a120d590..6ef7e11f2 100644
--- a/lib/protoparser/prometheus/streamparser_test.go
+++ b/lib/protoparser/prometheus/streamparser_test.go
@@ -8,11 +8,12 @@ import (
 )
 
 func TestParseStream(t *testing.T) {
+	const defaultTimestamp = 123
 	f := func(s string, rowsExpected []Row) {
 		t.Helper()
 		bb := bytes.NewBufferString(s)
 		var result []Row
-		err := ParseStream(bb, false, func(rows []Row) error {
+		err := ParseStream(bb, defaultTimestamp, false, func(rows []Row) error {
 			result = appendRowCopies(result, rows)
 			return nil
 		})
@@ -33,7 +34,7 @@ func TestParseStream(t *testing.T) {
 			t.Fatalf("unexpected error when closing gzip writer: %s", err)
 		}
 		result = nil
-		err = ParseStream(bb, true, func(rows []Row) error {
+		err = ParseStream(bb, defaultTimestamp, true, func(rows []Row) error {
 			result = appendRowCopies(result, rows)
 			return nil
 		})
@@ -67,6 +68,11 @@ func TestParseStream(t *testing.T) {
 			Timestamp: 4,
 		},
 	})
+	f("foo 23", []Row{{
+		Metric:    "foo",
+		Value:     23,
+		Timestamp: defaultTimestamp,
+	}})
 }
 
 func appendRowCopies(dst, src []Row) []Row {