Repository: VictoriaMetrics/VictoriaMetrics
Pull request #1143 (2021-03-23 10:47:48)

changes consul_service label value
according to prometheus discovery.
 It should mitigate issue with case sensetive services
https://github.com/hashicorp/consul/issues/5707

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143?src=pr&el=h1) Report
> Merging [#1143](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143?src=pr&el=desc) (4b8373e) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/71b72304ae8d652b7bb5cca98688f710a36f1ac4?el=desc) (71b7230) will **decrease** coverage by `0.00%`.
> The diff coverage is `25.00%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1143      +/-   ##
==========================================
- Coverage   60.20%   60.20%   -0.01%     
==========================================
  Files         213      213              
  Lines       25189    25189              
==========================================
- Hits        15165    15164       -1     
- Misses       8628     8629       +1     
  Partials     1396     1396              
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/promscrape/discovery/consul/watch.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC93YXRjaC5nbw==) | `0.00% <0.00%> (ø)` | |
| [lib/promscrape/discovery/consul/service\_node.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-bGliL3Byb21zY3JhcGUvZGlzY292ZXJ5L2NvbnN1bC9zZXJ2aWNlX25vZGUuZ28=) | `59.64% <50.00%> (ø)` | |
| [app/vmalert/remotewrite/remotewrite.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvcmVtb3Rld3JpdGUvcmVtb3Rld3JpdGUuZ28=) | `57.00% <0.00%> (-1.87%)` | :arrow_down: |
| [lib/storage/index\_db.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvaW5kZXhfZGIuZ28=) | `66.24% <0.00%> (-0.16%)` | :arrow_down: |
| [app/vmselect/promql/aggr\_incremental.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-YXBwL3Ztc2VsZWN0L3Byb21xbC9hZ2dyX2luY3JlbWVudGFsLmdv) | `87.98% <0.00%> (ø)` | |
| [lib/mergeset/table.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-bGliL21lcmdlc2V0L3RhYmxlLmdv) | `66.05% <0.00%> (+0.30%)` | :arrow_up: |
| [lib/storage/part.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvcGFydC5nbw==) | `73.04% <0.00%> (+1.73%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143?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/1143?src=pr&el=footer). Last update [e17eb35...4b8373e](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/1143?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 c55aad5f0..0468a5a43 100644
--- a/README.md
+++ b/README.md
@@ -605,6 +605,10 @@ VictoriaMetrics supports the following Graphite APIs, which are needed for [Grap
 
 All the Graphite handlers can be pre-pended with `/graphite` prefix. For example, both `/graphite/metrics/find` and `/metrics/find` should work.
 
+VictoriaMetrics accepts optional `extra_label=<label_name>=<label_value>` query arg for all the Graphite APIs. This arg can be used for limiting the scope of time series
+visible to the given tenant. It is expected that the `extra_label` query arg is automatically set by auth proxy sitting in front of VictoriaMetrics.
+[Contact us](mailto:sales@victoriametrics.com) if you need assistance with such a proxy.
+
 VictoriaMetrics supports `__graphite__` pseudo-label for filtering time series with Graphite-compatible filters in [MetricsQL](https://victoriametrics.github.io/MetricsQL.html).
 For example, `{__graphite__="foo.*.bar"}` is equivalent to `{__name__=~"foo[.][^.]*[.]bar"}`, but it works faster
 and it is easier to use when migrating from Graphite to VictoriaMetrics.
diff --git a/app/vmselect/graphite/tags_api.go b/app/vmselect/graphite/tags_api.go
index 5d80f1213..3344cb6ae 100644
--- a/app/vmselect/graphite/tags_api.go
+++ b/app/vmselect/graphite/tags_api.go
@@ -32,13 +32,17 @@ func TagsDelSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Re
 	var row graphiteparser.Row
 	var tagsPool []graphiteparser.Tag
 	ct := startTime.UnixNano() / 1e6
+	etfs, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
+	if err != nil {
+		return fmt.Errorf("cannot setup tag filters: %w", err)
+	}
 	for _, path := range paths {
 		var err error
 		tagsPool, err = row.UnmarshalMetricAndTags(path, tagsPool[:0])
 		if err != nil {
 			return fmt.Errorf("cannot parse path=%q: %w", path, err)
 		}
-		tfs := make([]storage.TagFilter, 0, 1+len(row.Tags))
+		tfs := make([]storage.TagFilter, 0, 1+len(row.Tags)+len(etfs))
 		tfs = append(tfs, storage.TagFilter{
 			Key:   nil,
 			Value: []byte(row.Metric),
@@ -49,6 +53,7 @@ func TagsDelSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Re
 				Value: []byte(tag.Value),
 			})
 		}
+		tfs = append(tfs, etfs...)
 		sq := storage.NewSearchQuery(0, ct, [][]storage.TagFilter{tfs})
 		n, err := netstorage.DeleteSeries(sq, deadline)
 		if err != nil {
@@ -176,7 +181,11 @@ func TagsAutoCompleteValuesHandler(startTime time.Time, w http.ResponseWriter, r
 	valuePrefix := r.FormValue("valuePrefix")
 	exprs := r.Form["expr"]
 	var tagValues []string
-	if len(exprs) == 0 {
+	etfs, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
+	if err != nil {
+		return fmt.Errorf("cannot setup tag filters: %w", err)
+	}
+	if len(exprs) == 0 && len(etfs) == 0 {
 		// Fast path: there are no `expr` filters, so use netstorage.GetGraphiteTagValues.
 		// Escape special chars in tagPrefix as Graphite does.
 		// See https://github.com/graphite-project/graphite-web/blob/3ad279df5cb90b211953e39161df416e54a84948/webapp/graphite/tags/base.py#L228
@@ -187,7 +196,7 @@ func TagsAutoCompleteValuesHandler(startTime time.Time, w http.ResponseWriter, r
 		}
 	} else {
 		// Slow path: use netstorage.SearchMetricNames for applying `expr` filters.
-		sq, err := getSearchQueryForExprs(startTime, exprs)
+		sq, err := getSearchQueryForExprs(startTime, etfs, exprs)
 		if err != nil {
 			return err
 		}
@@ -257,7 +266,11 @@ func TagsAutoCompleteTagsHandler(startTime time.Time, w http.ResponseWriter, r *
 	tagPrefix := r.FormValue("tagPrefix")
 	exprs := r.Form["expr"]
 	var labels []string
-	if len(exprs) == 0 {
+	etfs, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
+	if err != nil {
+		return fmt.Errorf("cannot setup tag filters: %w", err)
+	}
+	if len(exprs) == 0 && len(etfs) == 0 {
 		// Fast path: there are no `expr` filters, so use netstorage.GetGraphiteTags.
 
 		// Escape special chars in tagPrefix as Graphite does.
@@ -269,7 +282,7 @@ func TagsAutoCompleteTagsHandler(startTime time.Time, w http.ResponseWriter, r *
 		}
 	} else {
 		// Slow path: use netstorage.SearchMetricNames for applying `expr` filters.
-		sq, err := getSearchQueryForExprs(startTime, exprs)
+		sq, err := getSearchQueryForExprs(startTime, etfs, exprs)
 		if err != nil {
 			return err
 		}
@@ -332,7 +345,11 @@ func TagsFindSeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.R
 	if len(exprs) == 0 {
 		return fmt.Errorf("expecting at least one `expr` query arg")
 	}
-	sq, err := getSearchQueryForExprs(startTime, exprs)
+	etfs, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
+	if err != nil {
+		return fmt.Errorf("cannot setup tag filters: %w", err)
+	}
+	sq, err := getSearchQueryForExprs(startTime, etfs, exprs)
 	if err != nil {
 		return err
 	}
@@ -457,12 +474,13 @@ func getInt(r *http.Request, argName string) (int, error) {
 	return n, nil
 }
 
-func getSearchQueryForExprs(startTime time.Time, exprs []string) (*storage.SearchQuery, error) {
+func getSearchQueryForExprs(startTime time.Time, etfs []storage.TagFilter, exprs []string) (*storage.SearchQuery, error) {
 	tfs, err := exprsToTagFilters(exprs)
 	if err != nil {
 		return nil, err
 	}
 	ct := startTime.UnixNano() / 1e6
+	tfs = append(tfs, etfs...)
 	sq := storage.NewSearchQuery(0, ct, [][]storage.TagFilter{tfs})
 	return sq, nil
 }
diff --git a/app/vmselect/prometheus/prometheus.go b/app/vmselect/prometheus/prometheus.go
index e9e374333..8ab587d4f 100644
--- a/app/vmselect/prometheus/prometheus.go
+++ b/app/vmselect/prometheus/prometheus.go
@@ -279,7 +279,7 @@ func ExportHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 	if start >= end {
 		end = start + defaultStep
 	}
-	etf, err := getEnforcedTagFiltersFromRequest(r)
+	etf, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
 	if err != nil {
 		return err
 	}
@@ -472,7 +472,7 @@ func LabelValuesHandler(startTime time.Time, labelName string, w http.ResponseWr
 	if err := r.ParseForm(); err != nil {
 		return fmt.Errorf("cannot parse form values: %w", err)
 	}
-	etf, err := getEnforcedTagFiltersFromRequest(r)
+	etf, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
 	if err != nil {
 		return err
 	}
@@ -687,7 +687,7 @@ func LabelsHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
 	if err := r.ParseForm(); err != nil {
 		return fmt.Errorf("cannot parse form values: %w", err)
 	}
-	etf, err := getEnforcedTagFiltersFromRequest(r)
+	etf, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
 	if err != nil {
 		return err
 	}
@@ -952,7 +952,7 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
 	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)
 	}
-	etf, err := getEnforcedTagFiltersFromRequest(r)
+	etf, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
 	if err != nil {
 		return err
 	}
@@ -1086,7 +1086,7 @@ func QueryRangeHandler(startTime time.Time, w http.ResponseWriter, r *http.Reque
 	if err != nil {
 		return err
 	}
-	etf, err := getEnforcedTagFiltersFromRequest(r)
+	etf, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
 	if err != nil {
 		return err
 	}
@@ -1236,26 +1236,6 @@ func getMaxLookback(r *http.Request) (int64, error) {
 	return searchutils.GetDuration(r, "max_lookback", d)
 }
 
-func getEnforcedTagFiltersFromRequest(r *http.Request) ([]storage.TagFilter, error) {
-	// fast path.
-	extraLabels := r.Form["extra_label"]
-	if len(extraLabels) == 0 {
-		return nil, nil
-	}
-	tagFilters := make([]storage.TagFilter, 0, len(extraLabels))
-	for _, match := range extraLabels {
-		tmp := strings.SplitN(match, "=", 2)
-		if len(tmp) != 2 {
-			return nil, fmt.Errorf("`extra_label` query arg must have the format `name=value`; got %q", match)
-		}
-		tagFilters = append(tagFilters, storage.TagFilter{
-			Key:   []byte(tmp[0]),
-			Value: []byte(tmp[1]),
-		})
-	}
-	return tagFilters, nil
-}
-
 func addEnforcedFiltersToTagFilterss(dstTfss [][]storage.TagFilter, enforcedFilters []storage.TagFilter) [][]storage.TagFilter {
 	if len(dstTfss) == 0 {
 		return [][]storage.TagFilter{
@@ -1289,7 +1269,7 @@ func getTagFilterssFromRequest(r *http.Request) ([][]storage.TagFilter, error) {
 	if err != nil {
 		return nil, err
 	}
-	etf, err := getEnforcedTagFiltersFromRequest(r)
+	etf, err := searchutils.GetEnforcedTagFiltersFromRequest(r)
 	if err != nil {
 		return nil, err
 	}
diff --git a/app/vmselect/searchutils/searchutils.go b/app/vmselect/searchutils/searchutils.go
index c3c5c5511..1033f6ba1 100644
--- a/app/vmselect/searchutils/searchutils.go
+++ b/app/vmselect/searchutils/searchutils.go
@@ -9,6 +9,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
+
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
 	"github.com/VictoriaMetrics/metricsql"
 )
@@ -188,3 +190,24 @@ func (d *Deadline) String() string {
 	elapsed := time.Since(startTime)
 	return fmt.Sprintf("%.3f seconds (elapsed %.3f seconds); the timeout can be adjusted with `%s` command-line flag", d.timeout.Seconds(), elapsed.Seconds(), d.flagHint)
 }
+
+// GetEnforcedTagFiltersFromRequest returns additional filters from request.
+func GetEnforcedTagFiltersFromRequest(r *http.Request) ([]storage.TagFilter, error) {
+	// fast path.
+	extraLabels := r.Form["extra_label"]
+	if len(extraLabels) == 0 {
+		return nil, nil
+	}
+	tagFilters := make([]storage.TagFilter, 0, len(extraLabels))
+	for _, match := range extraLabels {
+		tmp := strings.SplitN(match, "=", 2)
+		if len(tmp) != 2 {
+			return nil, fmt.Errorf("`extra_label` query arg must have the format `name=value`; got %q", match)
+		}
+		tagFilters = append(tagFilters, storage.TagFilter{
+			Key:   []byte(tmp[0]),
+			Value: []byte(tmp[1]),
+		})
+	}
+	return tagFilters, nil
+}
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index c646f3f60..69f1abe5c 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -8,6 +8,7 @@
   * `process_resident_memory_shared_bytes` - RSS share for memory shared with other processes (aka shared memory). This share can be freed by the OS at any time, so it must be ignored by OOM killer.
   * `process_resident_memory_peak_bytes` - peak RSS usage for the process.
   * `process_virtual_memory_peak_bytes` - peak virtual memory usage for the process.
+* FEATURE: accept and enforce `extra_label=<label_name>=<label_value>` query arg at [Graphite APIs](https://victoriametrics.github.io/#graphite-api-usage).
 
 * BUGFIX: prevent from infinite loop on `{__graphite__="..."}` filters when a metric name contains `*`, `{` or `[` chars.
 * BUGFIX: prevent from infinite loop in `/metrics/find` and `/metrics/expand` [Graphite Metrics API handlers](https://victoriametrics.github.io/#graphite-metrics-api-usage) when they match metric names or labels with `*`, `{` or `[` chars.
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index c55aad5f0..0468a5a43 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -605,6 +605,10 @@ VictoriaMetrics supports the following Graphite APIs, which are needed for [Grap
 
 All the Graphite handlers can be pre-pended with `/graphite` prefix. For example, both `/graphite/metrics/find` and `/metrics/find` should work.
 
+VictoriaMetrics accepts optional `extra_label=<label_name>=<label_value>` query arg for all the Graphite APIs. This arg can be used for limiting the scope of time series
+visible to the given tenant. It is expected that the `extra_label` query arg is automatically set by auth proxy sitting in front of VictoriaMetrics.
+[Contact us](mailto:sales@victoriametrics.com) if you need assistance with such a proxy.
+
 VictoriaMetrics supports `__graphite__` pseudo-label for filtering time series with Graphite-compatible filters in [MetricsQL](https://victoriametrics.github.io/MetricsQL.html).
 For example, `{__graphite__="foo.*.bar"}` is equivalent to `{__name__=~"foo[.][^.]*[.]bar"}`, but it works faster
 and it is easier to use when migrating from Graphite to VictoriaMetrics.
diff --git a/lib/promscrape/discovery/consul/service_node.go b/lib/promscrape/discovery/consul/service_node.go
index 4086bd3b7..8023ecfd7 100644
--- a/lib/promscrape/discovery/consul/service_node.go
+++ b/lib/promscrape/discovery/consul/service_node.go
@@ -13,8 +13,10 @@ import (
 func getServiceNodesLabels(cfg *apiConfig) []map[string]string {
 	sns := cfg.consulWatcher.getServiceNodesSnapshot()
 	var ms []map[string]string
-	for _, sn := range sns {
-		ms = sn.appendTargetLabels(ms, cfg.tagSeparator)
+	for svc, sn := range sns {
+		for i := range sn {
+			ms = sn[i].appendTargetLabels(ms, svc, cfg.tagSeparator)
+		}
 	}
 	return ms
 }
@@ -67,7 +69,7 @@ func parseServiceNodes(data []byte) ([]ServiceNode, error) {
 	return sns, nil
 }
 
-func (sn *ServiceNode) appendTargetLabels(ms []map[string]string, tagSeparator string) []map[string]string {
+func (sn *ServiceNode) appendTargetLabels(ms []map[string]string, serviceName, tagSeparator string) []map[string]string {
 	var addr string
 	if sn.Service.Address != "" {
 		addr = discoveryutils.JoinHostPort(sn.Service.Address, sn.Service.Port)
@@ -80,7 +82,7 @@ func (sn *ServiceNode) appendTargetLabels(ms []map[string]string, tagSeparator s
 		"__meta_consul_dc":              sn.Node.Datacenter,
 		"__meta_consul_health":          aggregatedStatus(sn.Checks),
 		"__meta_consul_node":            sn.Node.Node,
-		"__meta_consul_service":         sn.Service.Service,
+		"__meta_consul_service":         serviceName,
 		"__meta_consul_service_address": sn.Service.Address,
 		"__meta_consul_service_id":      sn.Service.ID,
 		"__meta_consul_service_port":    strconv.Itoa(sn.Service.Port),
diff --git a/lib/promscrape/discovery/consul/watch.go b/lib/promscrape/discovery/consul/watch.go
index bafb3f183..4fcae3def 100644
--- a/lib/promscrape/discovery/consul/watch.go
+++ b/lib/promscrape/discovery/consul/watch.go
@@ -234,11 +234,11 @@ func (sw *serviceWatcher) watchForServiceNodesUpdates(cw *consulWatcher) {
 }
 
 // getServiceNodesSnapshot returns a snapshot of discovered ServiceNodes.
-func (cw *consulWatcher) getServiceNodesSnapshot() []ServiceNode {
-	var sns []ServiceNode
+func (cw *consulWatcher) getServiceNodesSnapshot() map[string][]ServiceNode {
 	cw.servicesLock.Lock()
-	for _, sw := range cw.services {
-		sns = append(sns, sw.serviceNodes...)
+	sns := make(map[string][]ServiceNode, len(cw.services))
+	for svc, sw := range cw.services {
+		sns[svc] = sw.serviceNodes
 	}
 	cw.servicesLock.Unlock()
 	return sns


Test patch:
diff --git a/app/vmselect/prometheus/prometheus_test.go b/app/vmselect/prometheus/prometheus_test.go
index d376a928b..037884999 100644
--- a/app/vmselect/prometheus/prometheus_test.go
+++ b/app/vmselect/prometheus/prometheus_test.go
@@ -2,7 +2,6 @@ package prometheus
 
 import (
 	"math"
-	"net/http"
 	"reflect"
 	"testing"
 
@@ -232,40 +231,3 @@ func Test_addEnforcedFiltersToTagFilterss(t *testing.T) {
 			{tfFromKV("l2", "v2"), tfFromKV("ext-l1", "v2")},
 		})
 }
-
-func Test_getEnforcedTagFiltersFromRequest(t *testing.T) {
-	httpReqWithForm := func(tfs []string) *http.Request {
-		return &http.Request{
-			Form: map[string][]string{
-				"extra_label": tfs,
-			},
-		}
-	}
-	f := func(t *testing.T, r *http.Request, want []storage.TagFilter, wantErr bool) {
-		t.Helper()
-		got, err := getEnforcedTagFiltersFromRequest(r)
-		if (err != nil) != wantErr {
-			t.Fatalf("unexpected error: %v", err)
-		}
-		if !reflect.DeepEqual(got, want) {
-			t.Fatalf("unxpected result for getEnforcedTagFiltersFromRequest, \ngot: %v,\n want: %v", want, got)
-		}
-	}
-
-	f(t, httpReqWithForm([]string{"label=value"}),
-		[]storage.TagFilter{
-			tfFromKV("label", "value"),
-		},
-		false)
-
-	f(t, httpReqWithForm([]string{"job=vmagent", "dc=gce"}),
-		[]storage.TagFilter{tfFromKV("job", "vmagent"), tfFromKV("dc", "gce")},
-		false,
-	)
-	f(t, httpReqWithForm([]string{"bad_filter"}),
-		nil,
-		true,
-	)
-	f(t, &http.Request{},
-		nil, false)
-}
diff --git a/app/vmselect/searchutils/searchutils_test.go b/app/vmselect/searchutils/searchutils_test.go
index c64484159..c47f72ede 100644
--- a/app/vmselect/searchutils/searchutils_test.go
+++ b/app/vmselect/searchutils/searchutils_test.go
@@ -4,7 +4,10 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"reflect"
 	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
 )
 
 func TestGetTimeSuccess(t *testing.T) {
@@ -76,3 +79,48 @@ func TestGetTimeError(t *testing.T) {
 	f("-292273086-05-16T16:47:07Z")
 	f("292277025-08-18T07:12:54.999999998Z")
 }
+
+// helper for tests
+func tfFromKV(k, v string) storage.TagFilter {
+	return storage.TagFilter{
+		Key:   []byte(k),
+		Value: []byte(v),
+	}
+}
+
+func TestGetEnforcedTagFiltersFromRequest(t *testing.T) {
+	httpReqWithForm := func(tfs []string) *http.Request {
+		return &http.Request{
+			Form: map[string][]string{
+				"extra_label": tfs,
+			},
+		}
+	}
+	f := func(t *testing.T, r *http.Request, want []storage.TagFilter, wantErr bool) {
+		t.Helper()
+		got, err := GetEnforcedTagFiltersFromRequest(r)
+		if (err != nil) != wantErr {
+			t.Fatalf("unexpected error: %v", err)
+		}
+		if !reflect.DeepEqual(got, want) {
+			t.Fatalf("unxpected result for getEnforcedTagFiltersFromRequest, \ngot: %v,\n want: %v", want, got)
+		}
+	}
+
+	f(t, httpReqWithForm([]string{"label=value"}),
+		[]storage.TagFilter{
+			tfFromKV("label", "value"),
+		},
+		false)
+
+	f(t, httpReqWithForm([]string{"job=vmagent", "dc=gce"}),
+		[]storage.TagFilter{tfFromKV("job", "vmagent"), tfFromKV("dc", "gce")},
+		false,
+	)
+	f(t, httpReqWithForm([]string{"bad_filter"}),
+		nil,
+		true,
+	)
+	f(t, &http.Request{},
+		nil, false)
+}
diff --git a/lib/promscrape/discovery/consul/service_node_test.go b/lib/promscrape/discovery/consul/service_node_test.go
index 0b65a1f20..7b5e3dc08 100644
--- a/lib/promscrape/discovery/consul/service_node_test.go
+++ b/lib/promscrape/discovery/consul/service_node_test.go
@@ -106,7 +106,7 @@ func TestParseServiceNodesSuccess(t *testing.T) {
 
 	// Check sn.appendTargetLabels()
 	tagSeparator := ","
-	labelss := sn.appendTargetLabels(nil, tagSeparator)
+	labelss := sn.appendTargetLabels(nil, "redis", tagSeparator)
 	var sortedLabelss [][]prompbmarshal.Label
 	for _, labels := range labelss {
 		sortedLabelss = append(sortedLabelss, discoveryutils.GetSortedLabels(labels))