Repository: VictoriaMetrics/VictoriaMetrics
Pull request #5778 (2024-02-08 08:44:21)

Enable the `limit` query param for the `/select/logsql/query`
Enable the `limit` query param for the `/select/logsql/query` victoria logs API endpoint. 

Related issue: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5674


Patch:
diff --git a/README.md b/README.md
index b0186782e..0ec596d2d 100644
--- a/README.md
+++ b/README.md
@@ -18,24 +18,27 @@ VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMet
 [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Snap packages](https://snapcraft.io/victoriametrics)
 and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). 
 
-The cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+Documentation for the cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
 
 Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow the 
 [quick start guide](https://docs.victoriametrics.com/Quick-Start.html) for a better experience.
 
-There is also a user-friendly database for logs - [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/).
-
 If you have questions about VictoriaMetrics, then feel free asking them in the [VictoriaMetrics community Slack chat](https://slack.victoriametrics.com/).
 
-[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. 
+[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics.
 See [features available in enterprise package](https://docs.victoriametrics.com/enterprise.html).
-Enterprise binaries can be downloaded and evaluated for free 
+Enterprise binaries can be downloaded and evaluated for free
 from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
 You can also [request a free trial license](https://victoriametrics.com/products/enterprise/trial/).
 
-VictoriaMetrics is developed at a fast pace, so it is recommended to check the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) periodically, and to perform [regular upgrades](#how-to-upgrade-victoriametrics).
+VictoriaMetrics is developed at a fast pace, so it is recommended to check the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) periodically,
+and to perform [regular upgrades](#how-to-upgrade-victoriametrics).
+
+[VictoriaMetrics enterprise](https://docs.victoriametrics.com/enterprise/) provides long-term support lines of releases (LTS releases) -
+see [these docs](https://docs.victoriametrics.com/LTS-releases.md).
 
-VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services. We apply strict security measures in everything we do. See our [Security page](https://victoriametrics.com/security/) for more details.
+VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services.
+We apply strict security measures in everything we do. See [Security page](https://victoriametrics.com/security/) for more details.
 
 ## Prominent features
 
@@ -102,6 +105,23 @@ VictoriaMetrics has the following prominent features:
 
 See [case studies for VictoriaMetrics](https://docs.victoriametrics.com/CaseStudies.html) and [various Articles about VictoriaMetrics](https://docs.victoriametrics.com/Articles.html).
 
+## Components
+
+VictoriaMetrics ecosystem contains the following components additionally to [single-node VictoriaMetrics](https://docs.victoriametrics.com/):
+
+- [vmagent](https://docs.victoriametrics.com/vmagent/) - lightweight agent for receiving metrics via [pull-based](https://docs.victoriametrics.com/vmagent/#how-to-collect-metrics-in-prometheus-format)
+  and [push-based](https://docs.victoriametrics.com/vmagent/#how-to-push-data-to-vmagent) protocols, transforming and sending them to the configured Prometheus-compatible
+  remote storage systems such as VictoriaMetrics.
+- [vmalert](https://docs.victoriametrics.com/vmalert/) - a service for processing Prometheus-compatible alerting and recording rules.
+- [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/) -  a tool for validating alerting and recording rules.
+- [vmauth](https://docs.victoriametrics.com/vmauth/) - authorization proxy and load balancer optimized for VictoriaMetrics products.
+- [vmgateway](https://docs.victoriametrics.com/vmgateway/) - auhtorization proxy with per-[tenant](https://docs.victoriametrics.com/cluster-victoriametrics/#multitenancy) rate limiting cababilities.
+- [vmctl](https://docs.victoriametrics.com/vmctl/) - a tool for migrating and copying data between different storage systems for metrics.
+- [vmbackup](https://docs.victoriametrics.com/vmbackup/), [vmrestore](https://docs.victoriametrics.com/vmrestore/) and [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager/) -
+  tools for creating backups and restoring from backups for VictoriaMetrics data.
+- `vminsert`, `vmselect` and `vmstorage` - components of [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+- [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/) - user-friendly cost-efficient database for logs.
+
 ## Operation
 
 ### Install
diff --git a/SECURITY.md b/SECURITY.md
index 9226abdc0..8d1221d21 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,13 +2,17 @@
 
 ## Supported Versions
 
+The following versions of VictoriaMetrics receive regular security fixes:
+
 | Version | Supported          |
 |---------|--------------------|
 | [latest release](https://docs.victoriametrics.com/CHANGELOG.html) | :white_check_mark: |
-| v1.97.x LTS release | :white_check_mark: |
-| v1.93.x LTS release | :white_check_mark: |
+| v1.97.x [LTS line](https://docs.victoriametrics.com/LTS-releases.html) | :white_check_mark: |
+| v1.93.x [LTS line](https://docs.victoriametrics.com/LTS-releases.html) | :white_check_mark: |
 | other releases  | :x:                |
 
+See [this page](https://victoriametrics.com/security/) for more details.
+
 ## Reporting a Vulnerability
 
 Please report any security issues to security@victoriametrics.com
diff --git a/app/vlselect/logsql/logsql.go b/app/vlselect/logsql/logsql.go
index f525335be..343593f44 100644
--- a/app/vlselect/logsql/logsql.go
+++ b/app/vlselect/logsql/logsql.go
@@ -2,11 +2,13 @@ package logsql
 
 import (
 	"net/http"
+	"sync"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vlstorage"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
 )
 
@@ -31,23 +33,36 @@ func ProcessQueryRequest(w http.ResponseWriter, r *http.Request, stopCh <-chan s
 		httpserver.Errorf(w, r, "cannot parse query [%s]: %s", qStr, err)
 		return
 	}
+	limit, err := httputils.GetInt(r, "limit")
+	if err != nil {
+		httpserver.Errorf(w, r, "cannot parse limit from the request: %s", err)
+		return
+	}
 	w.Header().Set("Content-Type", "application/stream+json; charset=utf-8")
-
 	sw := getSortWriter()
 	sw.Init(w, maxSortBufferSize.IntN())
 	tenantIDs := []logstorage.TenantID{tenantID}
-	vlstorage.RunQuery(tenantIDs, q, stopCh, func(columns []logstorage.BlockColumn) {
+
+	var mx sync.Mutex
+	vlstorage.RunQuery(tenantIDs, q, stopCh, func(columns []logstorage.BlockColumn) bool {
 		if len(columns) == 0 {
-			return
+			return true
 		}
 		rowsCount := len(columns[0].Values)
-
+		mx.Lock()
+		if rowsCount > limit {
+			rowsCount = limit
+		}
+		limit = limit - rowsCount
+		mx.Unlock()
 		bb := blockResultPool.Get()
 		for rowIdx := 0; rowIdx < rowsCount; rowIdx++ {
 			WriteJSONRow(bb, columns, rowIdx)
 		}
 		sw.MustWrite(bb.B)
 		blockResultPool.Put(bb)
+
+		return limit == 0
 	})
 	sw.FinalFlush()
 	putSortWriter(sw)
diff --git a/app/vlstorage/main.go b/app/vlstorage/main.go
index b1b55675c..ed5eba88d 100644
--- a/app/vlstorage/main.go
+++ b/app/vlstorage/main.go
@@ -100,7 +100,7 @@ func MustAddRows(lr *logstorage.LogRows) {
 }
 
 // RunQuery runs the given q and calls processBlock for the returned data blocks
-func RunQuery(tenantIDs []logstorage.TenantID, q *logstorage.Query, stopCh <-chan struct{}, processBlock func(columns []logstorage.BlockColumn)) {
+func RunQuery(tenantIDs []logstorage.TenantID, q *logstorage.Query, stopCh <-chan struct{}, processBlock func(columns []logstorage.BlockColumn) bool) {
 	strg.RunQuery(tenantIDs, q, stopCh, processBlock)
 }
 
diff --git a/app/vmctl/flags.go b/app/vmctl/flags.go
index 01d0a7dbb..7aa227cd7 100644
--- a/app/vmctl/flags.go
+++ b/app/vmctl/flags.go
@@ -208,6 +208,11 @@ const (
 	influxMeasurementFieldSeparator = "influx-measurement-field-separator"
 	influxSkipDatabaseLabel         = "influx-skip-database-label"
 	influxPrometheusMode            = "influx-prometheus-mode"
+	influxCertFile                  = "influx-cert-file"
+	influxKeyFile                   = "influx-key-file"
+	influxCAFile                    = "influx-CA-file"
+	influxServerName                = "influx-server-name"
+	influxInsecureSkipVerify        = "influx-insecure-skip-verify"
 )
 
 var (
@@ -272,7 +277,28 @@ var (
 		},
 		&cli.BoolFlag{
 			Name:  influxPrometheusMode,
-			Usage: "Wether to restore the original timeseries name previously written from Prometheus to InfluxDB v1 via remote_write.",
+			Usage: "Whether to restore the original timeseries name previously written from Prometheus to InfluxDB v1 via remote_write.",
+			Value: false,
+		},
+		&cli.StringFlag{
+			Name:  influxCertFile,
+			Usage: "Optional path to client-side TLS certificate file to use when connecting to -influx-addr",
+		},
+		&cli.StringFlag{
+			Name:  influxKeyFile,
+			Usage: "Optional path to client-side TLS key to use when connecting to -influx-addr",
+		},
+		&cli.StringFlag{
+			Name:  influxCAFile,
+			Usage: "Optional path to TLS CA file to use for verifying connections to -influx-addr. By default, system CA is used",
+		},
+		&cli.StringFlag{
+			Name:  influxServerName,
+			Usage: "Optional TLS server name to use for connections to -influx-addr. By default, the server name from -influx-addr is used",
+		},
+		&cli.BoolFlag{
+			Name:  influxInsecureSkipVerify,
+			Usage: "Whether to skip tls verification when connecting to -influx-addr",
 			Value: false,
 		},
 	}
@@ -496,6 +522,10 @@ const (
 	remoteReadPassword           = "remote-read-password"
 	remoteReadHTTPTimeout        = "remote-read-http-timeout"
 	remoteReadHeaders            = "remote-read-headers"
+	remoteReadCertFile           = "remote-read-cert-file"
+	remoteReadKeyFile            = "remote-read-key-file"
+	remoteReadCAFile             = "remote-read-CA-file"
+	remoteReadServerName         = "remote-read-server-name"
 	remoteReadInsecureSkipVerify = "remote-read-insecure-skip-verify"
 	remoteReadDisablePathAppend  = "remote-read-disable-path-append"
 )
@@ -574,6 +604,22 @@ var (
 				"For example, --remote-read-headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding remote source storage. \n" +
 				"Multiple headers must be delimited by '^^': --remote-read-headers='header1:value1^^header2:value2'",
 		},
+		&cli.StringFlag{
+			Name:  remoteReadCertFile,
+			Usage: "Optional path to client-side TLS certificate file to use when connecting to -remote-read-src-addr",
+		},
+		&cli.StringFlag{
+			Name:  remoteReadKeyFile,
+			Usage: "Optional path to client-side TLS key to use when connecting to -remote-read-src-addr",
+		},
+		&cli.StringFlag{
+			Name:  remoteReadCAFile,
+			Usage: "Optional path to TLS CA file to use for verifying connections to -remote-read-src-addr. By default, system CA is used",
+		},
+		&cli.StringFlag{
+			Name:  remoteReadServerName,
+			Usage: "Optional TLS server name to use for connections to remoteReadSrcAddr. By default, the server name from -remote-read-src-addr is used",
+		},
 		&cli.BoolFlag{
 			Name:  remoteReadInsecureSkipVerify,
 			Usage: "Whether to skip TLS certificate verification when connecting to the remote read address",
diff --git a/app/vmctl/influx/influx.go b/app/vmctl/influx/influx.go
index 13b374a63..3b1c4c482 100644
--- a/app/vmctl/influx/influx.go
+++ b/app/vmctl/influx/influx.go
@@ -1,6 +1,7 @@
 package influx
 
 import (
+	"crypto/tls"
 	"fmt"
 	"io"
 	"log"
@@ -33,7 +34,8 @@ type Config struct {
 	Retention string
 	ChunkSize int
 
-	Filter Filter
+	Filter    Filter
+	TLSConfig *tls.Config
 }
 
 // Filter contains configuration for filtering
@@ -86,10 +88,10 @@ type LabelPair struct {
 // configured with passed Config
 func NewClient(cfg Config) (*Client, error) {
 	c := influx.HTTPConfig{
-		Addr:               cfg.Addr,
-		Username:           cfg.Username,
-		Password:           cfg.Password,
-		InsecureSkipVerify: true,
+		Addr:      cfg.Addr,
+		Username:  cfg.Username,
+		Password:  cfg.Password,
+		TLSConfig: cfg.TLSConfig,
 	}
 	hc, err := influx.NewHTTPClient(c)
 	if err != nil {
diff --git a/app/vmctl/main.go b/app/vmctl/main.go
index 96cd936f2..78913790b 100644
--- a/app/vmctl/main.go
+++ b/app/vmctl/main.go
@@ -25,6 +25,7 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/common"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/native/stream"
 )
@@ -84,6 +85,18 @@ func main() {
 				Action: func(c *cli.Context) error {
 					fmt.Println("InfluxDB import mode")
 
+					// create TLS config
+					certFile := c.String(influxCertFile)
+					keyFile := c.String(influxKeyFile)
+					caFile := c.String(influxCAFile)
+					serverName := c.String(influxServerName)
+					insecureSkipVerify := c.Bool(influxInsecureSkipVerify)
+
+					tc, err := httputils.TLSConfig(certFile, caFile, keyFile, serverName, insecureSkipVerify)
+					if err != nil {
+						return fmt.Errorf("failed to create TLS Config: %s", err)
+					}
+
 					iCfg := influx.Config{
 						Addr:      c.String(influxAddr),
 						Username:  c.String(influxUser),
@@ -96,7 +109,9 @@ func main() {
 							TimeEnd:   c.String(influxFilterTimeEnd),
 						},
 						ChunkSize: c.Int(influxChunkSize),
+						TLSConfig: tc,
 					}
+
 					influxClient, err := influx.NewClient(iCfg)
 					if err != nil {
 						return fmt.Errorf("failed to create influx client: %s", err)
@@ -134,6 +149,10 @@ func main() {
 						Headers:            c.String(remoteReadHeaders),
 						LabelName:          c.String(remoteReadFilterLabel),
 						LabelValue:         c.String(remoteReadFilterLabelValue),
+						CertFile:           c.String(remoteReadCertFile),
+						KeyFile:            c.String(remoteReadKeyFile),
+						CAFile:             c.String(remoteReadCAFile),
+						ServerName:         c.String(remoteReadServerName),
 						InsecureSkipVerify: c.Bool(remoteReadInsecureSkipVerify),
 						DisablePathAppend:  c.Bool(remoteReadDisablePathAppend),
 					})
diff --git a/app/vmctl/remoteread/remoteread.go b/app/vmctl/remoteread/remoteread.go
index 5752ac08a..349fbb165 100644
--- a/app/vmctl/remoteread/remoteread.go
+++ b/app/vmctl/remoteread/remoteread.go
@@ -11,9 +11,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/utils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
 	"github.com/gogo/protobuf/proto"
 	"github.com/golang/snappy"
 	"github.com/prometheus/prometheus/prompb"
@@ -64,6 +64,13 @@ type Config struct {
 	// LabelName, LabelValue stands for label=~value pair used for read requests.
 	// Is optional.
 	LabelName, LabelValue string
+
+	// Optional cert file, key file, CA file and server name for client side TLS configuration
+	CertFile   string
+	KeyFile    string
+	CAFile     string
+	ServerName string
+
 	// TLSSkipVerify defines whether to skip TLS certificate verification when connecting to the remote read address.
 	InsecureSkipVerify bool
 }
@@ -103,10 +110,15 @@ func NewClient(cfg Config) (*Client, error) {
 		}
 	}
 
+	tr, err := httputils.Transport(cfg.Addr, cfg.CertFile, cfg.KeyFile, cfg.CAFile, cfg.ServerName, cfg.InsecureSkipVerify)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create transport: %s", err)
+	}
+
 	c := &Client{
 		c: &http.Client{
 			Timeout:   cfg.Timeout,
-			Transport: utils.Transport(cfg.Addr, cfg.InsecureSkipVerify),
+			Transport: tr,
 		},
 		addr:              strings.TrimSuffix(cfg.Addr, "/"),
 		disablePathAppend: cfg.DisablePathAppend,
diff --git a/app/vmctl/utils/time.go b/app/vmctl/time.go
similarity index 86%
rename from app/vmctl/utils/time.go
rename to app/vmctl/time.go
index 9b3b496b4..43dd7588d 100644
--- a/app/vmctl/utils/time.go
+++ b/app/vmctl/time.go
@@ -1,4 +1,4 @@
-package utils
+package main
 
 import (
 	"fmt"
@@ -13,8 +13,7 @@ const (
 	maxTimeMsecs = int64(1<<63-1) / 1e6
 )
 
-// GetTime  returns time from the given string.
-func GetTime(s string) (time.Time, error) {
+func parseTime(s string) (time.Time, error) {
 	secs, err := promutils.ParseTime(s)
 	if err != nil {
 		return time.Time{}, fmt.Errorf("cannot parse %s: %w", s, err)
diff --git a/app/vmctl/utils/tls.go b/app/vmctl/utils/tls.go
deleted file mode 100644
index fc0eb3d00..000000000
--- a/app/vmctl/utils/tls.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package utils
-
-import (
-	"crypto/tls"
-	"net/http"
-	"strings"
-)
-
-// Transport creates http.Transport object based on provided URL.
-// Returns Transport with TLS configuration if URL contains `https` prefix
-func Transport(URL string, insecureSkipVerify bool) *http.Transport {
-	t := http.DefaultTransport.(*http.Transport).Clone()
-	if !strings.HasPrefix(URL, "https") {
-		return t
-	}
-	t.TLSClientConfig = TLSConfig(insecureSkipVerify)
-	return t
-}
-
-// TLSConfig creates tls.Config object from provided arguments
-func TLSConfig(insecureSkipVerify bool) *tls.Config {
-	return &tls.Config{
-		InsecureSkipVerify: insecureSkipVerify,
-	}
-}
diff --git a/app/vmctl/vm_native.go b/app/vmctl/vm_native.go
index 3cbea821d..31375ae3d 100644
--- a/app/vmctl/vm_native.go
+++ b/app/vmctl/vm_native.go
@@ -16,7 +16,6 @@ import (
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/limiter"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/stepper"
-	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/utils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutils"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -54,14 +53,14 @@ func (p *vmNativeProcessor) run(ctx context.Context) error {
 		startTime: time.Now(),
 	}
 
-	start, err := utils.GetTime(p.filter.TimeStart)
+	start, err := parseTime(p.filter.TimeStart)
 	if err != nil {
 		return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeStart, p.filter.TimeStart, err)
 	}
 
 	end := time.Now().In(start.Location())
 	if p.filter.TimeEnd != "" {
-		end, err = utils.GetTime(p.filter.TimeEnd)
+		end, err = parseTime(p.filter.TimeEnd)
 		if err != nil {
 			return fmt.Errorf("failed to parse %s, provided: %s, error: %w", vmNativeFilterTimeEnd, p.filter.TimeEnd, err)
 		}
diff --git a/dashboards/victoriametrics-cluster.json b/dashboards/victoriametrics-cluster.json
index 48251c870..2ef8cf720 100644
--- a/dashboards/victoriametrics-cluster.json
+++ b/dashboards/victoriametrics-cluster.json
@@ -6210,7 +6210,7 @@
             "type": "prometheus",
             "uid": "$ds"
           },
-          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to vmselects.\n* Reads show traffic received from vminserts.",
+          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to clients.\n* Reads show traffic received from clients.",
           "fieldConfig": {
             "defaults": {
               "color": {
@@ -6310,10 +6310,10 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from {{name}}",
               "range": true,
               "refId": "A"
             },
@@ -6323,11 +6323,11 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to {{name}}",
               "range": true,
               "refId": "B"
             }
@@ -7420,7 +7420,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -7434,7 +7434,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -7550,7 +7550,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -7564,7 +7564,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
@@ -8442,7 +8442,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -8456,7 +8456,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -8572,7 +8572,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -8586,7 +8586,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
diff --git a/dashboards/vm/victoriametrics-cluster.json b/dashboards/vm/victoriametrics-cluster.json
index 30f60d526..eac1a5451 100644
--- a/dashboards/vm/victoriametrics-cluster.json
+++ b/dashboards/vm/victoriametrics-cluster.json
@@ -6211,7 +6211,7 @@
             "type": "victoriametrics-datasource",
             "uid": "$ds"
           },
-          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to vmselects.\n* Reads show traffic received from vminserts.",
+          "description": "Shows network usage by vmstorage services.\n* Writes show traffic sent to clients.\n* Reads show traffic received from clients.",
           "fieldConfig": {
             "defaults": {
               "color": {
@@ -6311,10 +6311,10 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from {{name}}",
               "range": true,
               "refId": "A"
             },
@@ -6324,11 +6324,11 @@
                 "uid": "$ds"
               },
               "editorMode": "code",
-              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
+              "expr": "sum(rate(vm_tcplistener_written_bytes_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])) by(name) * 8 > 0",
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to {{name}}",
               "range": true,
               "refId": "B"
             }
@@ -7421,7 +7421,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -7435,7 +7435,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -7551,7 +7551,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_select\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -7565,7 +7565,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
@@ -8443,7 +8443,7 @@
               "expr": "sum(rate(vm_tcplistener_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from client",
               "range": true,
               "refId": "A"
             },
@@ -8457,7 +8457,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to client",
               "range": true,
               "refId": "B"
             }
@@ -8573,7 +8573,7 @@
               "expr": "sum(rate(vm_tcpdialer_read_bytes_total{job=~\"$job_insert\", instance=~\"$instance\"}[$__rate_interval])) * 8 > 0",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "read",
+              "legendFormat": "read from vmstorage",
               "range": true,
               "refId": "A"
             },
@@ -8587,7 +8587,7 @@
               "format": "time_series",
               "hide": false,
               "intervalFactor": 1,
-              "legendFormat": "write ",
+              "legendFormat": "write to vmstorage",
               "range": true,
               "refId": "B"
             }
diff --git a/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml b/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml
index 3b4af7ce7..5474947b9 100644
--- a/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml
+++ b/deployment/docker/vmanomaly/vmanomaly-integration/docker-compose.yml
@@ -73,7 +73,7 @@ services:
     restart: always
   vmanomaly:
     container_name: vmanomaly
-    image: victoriametrics/vmanomaly:v1.9.2
+    image: victoriametrics/vmanomaly:v1.10.0
     depends_on:
       - "victoriametrics"
     ports:
diff --git a/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml b/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml
index 9990779b1..dd3c20ccf 100644
--- a/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml
+++ b/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_config.yml
@@ -3,10 +3,11 @@ scheduler:
   fit_every: "2m"
   fit_window: "3h"
 
-model:
-  class: "model.prophet.ProphetModel"
-  args:
-    interval_width: 0.98
+models:
+  prophet:
+    class: "model.prophet.ProphetModel"
+    args:
+      interval_width: 0.98
 
 reader:
   datasource_url: "http://victoriametrics:8428/"
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 3c46f4d44..d8e747b4e 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -23,12 +23,23 @@ The following `tip` changes can be tested by building VictoriaMetrics components
 
 Metrics of the latest version of VictoriaMetrics cluster are available for viewing at our
 [sandbox](https://play-grafana.victoriametrics.com/d/oS7Bi_0Wz_vm/victoriametrics-cluster-vm).
-The sandbox cluster installation is running under the constant load generated by
-[prometheus-benchmark](https://github.com/VictoriaMetrics/prometheus-benchmark) and used for testing latest releases.
+The sandbox cluster installation runs under the constant load generated by
+[prometheus-benchmark](https://github.com/VictoriaMetrics/prometheus-benchmark) and is used for testing latest releases.
+
+See also [LTS releases](https://docs.victoriametrics.com/LTS-releases.html).
 
 ## tip
 
+* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): propagate [label filters](https://docs.victoriametrics.com/keyconcepts/#filtering) via all the [label manipulation functions](https://docs.victoriametrics.com/metricsql/#label-manipulation-functions). For example, `label_del(some_metric{job="foo"}, "instance") + other_metric{pod="bar"}` is now transformed to `label_del(some_metric{job="foo",pod="bar"}, "instance") + other_metric{job="foo",pod="bar"}`. This should reduce the amounts of time series processed during query execution.
 * FEATURE: [Single-node VictoriaMetrics](https://docs.victoriametrics.com/) and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): expose `vm_last_partition_parts` [metrics](https://docs.victoriametrics.com/#monitoring), which show the number of [parts in the latest partition](https://docs.victoriametrics.com/#storage). These metrics may help debugging query performance slowdown related to the increased number of parts in the last partition, since usually all the ingested data is written to the last partition and all the queries are performed over the recently ingested data, e.g. the last partition.
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for `client_id` option into [kuma_sd_configs](https://docs.victoriametrics.com/sd_configs/#kuma_sd_configs) in the same way as Prometheus does. See [this pull request](https://github.com/prometheus/prometheus/pull/13278).
+* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for `enable_compression` option in [scrape_configs](https://docs.victoriametrics.com/sd_configs/#scrape_configs) in order to be compatible with Prometheus scrape configs. See [this pull request](https://github.com/prometheus/prometheus/pull/13166) and [this feature request](https://github.com/prometheus/prometheus/issues/12319). Note that `vmagent` was always supporting [`disable_compression` option](https://docs.victoriametrics.com/vmagent/#scrape_config-enhancements) before Prometheus added `enable_compression` option.
+* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): support client-side TLS configuration for [InfluxDB](https://docs.victoriametrics.com/vmctl/#migrating-data-from-influxdb-1x). See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5748). Thanks to @khushijain21 for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5783).
+* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): support client-side TLS configuration for [Remote Read protocol](https://docs.victoriametrics.com/vmctl/#migrating-data-by-remote-read-protocol). See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5748). Thanks to @khushijain21 for [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5798).
+* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): preserve [`WITH` templates](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) when clicking the `prettify query` button at the right side of query input field. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5383).
+
+* BUGFIX: fix the misleading error `0ms is out of allowed range [0 ...` when passing `step=0` to [/api/v1/query](https://docs.victoriametrics.com/keyconcepts/#instant-query)
+  or [/api/v1/query_range](https://docs.victoriametrics.com/keyconcepts/#range-query). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5795).
 
 ## [v1.98.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.98.0)
 
@@ -52,6 +63,7 @@ Released at 2024-02-14
 * FEATURE: [dashboards/all](https://grafana.com/orgs/victoriametrics): add new panel `CPU spent on GC`. It should help identifying cases when too much CPU is spent on garbage collection, and advice users on how this can be addressed.
 * FEATURE: [vmalert](https://docs.victoriametrics.com/#vmalert): support [filtering](https://prometheus.io/docs/prometheus/2.49/querying/api/#rules) for `/api/v1/rules` API. See [the pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5749) by @victoramsantos.
 * FEATURE: [vmbackup](https://docs.victoriametrics.com/vmbackup.html): support client-side TLS configuration for creating and deleting snapshots via `-snapshot.tls*` cmd-line flags. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5724). Thanks to @khushijain21 for the [pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5738).
+* FEATURE: enable `limit` query param for `/select/logsql/query` VictoriaLogs API endpoint. By default this limit sets to 1000 lines, but it can be changed via query param. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5674).
 
 * BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): reduce CPU usage when `-promscrape.dropOriginalLabels` command-line flag is set. This issue has been introduced in [v1.96.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.96.0) when addressing [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5389).
 * BUGFIX: [vmauth](https://docs.victoriametrics.com/vmauth.html): properly release memory during config reload. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4690).
diff --git a/docs/LTS-releases.md b/docs/LTS-releases.md
new file mode 100644
index 000000000..70ea3fe84
--- /dev/null
+++ b/docs/LTS-releases.md
@@ -0,0 +1,27 @@
+---
+sort: 300
+weight: 300
+title: Long-term support releases
+menu:
+  docs:
+    parent: 'victoriametrics'
+    weight: 300
+aliases:
+- /LTS-releases.html
+---
+
+# Long-term support releases
+
+[Enterprise version of VictoriaMetrics](https://docs.victoriametrics.com/enterprise/) provides long-term support lines of releases (aka LTS releases).
+Every LTS line receives bugfixes and [security fixes](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/SECURITY.md) for 12 months after
+the initial release. New LTS lines are published every 6 months, so the latest two LTS lines are supported at any given moment. This gives up to 6 months
+for the migration to new LTS lines for [VictoriaMetrics Enterprise](https://docs.victoriametrics.com/enterprise/) users.
+
+All the bugfixes and security fixes, which are included in LTS releases, are also available in [the latest release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest),
+so non-enterprise users are advised to regularly [upgrade](https://docs.victoriametrics.com/#how-to-upgrade-victoriametrics) VictoriaMetrics products
+to [the latest available releases](https://docs.victoriametrics.com/CHANGELOG.html).
+
+## Currently supported LTS release lines
+
+- 1.97.x - the latest one is [v1.97.2 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.97.2)
+- 1.93.x - the latest one is [v1.93.12 LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.93.12)
diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md
index 2b2266f13..7bebe89b2 100644
--- a/docs/MetricsQL.md
+++ b/docs/MetricsQL.md
@@ -26,12 +26,18 @@ and introduction into [basic querying via MetricsQL](https://docs.victoriametric
 
 The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
 
-* MetricsQL takes into account the previous point before the window in square brackets for range functions such as [rate](#rate) and [increase](#increase).
-  This allows returning the exact results users expect for `increase(metric[$__interval])` queries instead of incomplete results Prometheus returns for such queries.
-* MetricsQL doesn't extrapolate range function results. This addresses [this issue from Prometheus](https://github.com/prometheus/prometheus/issues/3746).
+* MetricsQL takes into account the last [raw sample](https://docs.victoriametrics.com/keyconcepts/#raw-samples) before the lookbehind window
+  in square brackets for [increase](#increase) and [rate](#rate) functions. This allows returning the exact results users expect for `increase(metric[$__interval])` queries
+  instead of incomplete results Prometheus returns for such queries. Prometheus misses the increase between the last sample before the lookbehind window
+  and the first sample inside the lookbehind window.
+* MetricsQL doesn't extrapolate [rate](#rate) and [increase](#increase) function results, so it always returns the expected results. For example, it returns
+  integer results from `increase()` over slow-changing integer counter. Prometheus in this case returns unexpected fractional results,
+  which may significantly differ from the expected results. This addresses [this issue from Prometheus](https://github.com/prometheus/prometheus/issues/3746).
   See technical details about VictoriaMetrics and Prometheus calculations for [rate](#rate)
   and [increase](#increase) [in this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1215#issuecomment-850305711).
-* MetricsQL returns the expected non-empty responses for [rate](#rate) with `step` values smaller than scrape interval.
+* MetricsQL returns the expected non-empty responses for [rate](#rate) function when Grafana or [vmui](https://docs.victoriametrics.com/#vmui)
+  passes `step` values smaller than the interval between [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples)
+  to [/api/v1/query_range](https://docs.victoriametrics.com/keyconcepts/#range-query).
   This addresses [this issue from Grafana](https://github.com/grafana/grafana/issues/11451).
   See also [this blog post](https://www.percona.com/blog/2020/02/28/better-prometheus-rate-function-with-victoriametrics/).
 * MetricsQL treats `scalar` type the same as `instant vector` without labels, since subtle differences between these types usually confuse users.
@@ -61,13 +67,14 @@ The list of MetricsQL features on top of PromQL:
 
 * Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
   See [these docs](https://docs.victoriametrics.com/#selecting-graphite-metrics).
-  VictoriaMetrics also can be used as Graphite datasource in Grafana.
-  See [these docs](https://docs.victoriametrics.com/#graphite-api-usage) for details.
+  VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/#graphite-api-usage) for details.
   See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
-* Lookbehind window in square brackets may be omitted. VictoriaMetrics automatically selects the lookbehind window
-  depending on the current step used for building the graph (e.g. `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query)).
+* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
+  depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query)
+  and the real interval between [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples) (aka `scrape_interval`).
   For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
-  It is equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
+  It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
+  The difference is documented in [rate() docs](#rate).
 * Numeric values can contain `_` delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
 * [Series selectors](https://docs.victoriametrics.com/keyConcepts.html#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
   selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
@@ -117,7 +124,8 @@ The list of MetricsQL features on top of PromQL:
   Go to [WITH templates playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) and try it.
 * String literals may be concatenated. This is useful with `WITH` templates:
   `WITH (commonPrefix="long_metric_prefix_") {__name__=commonPrefix+"suffix1"} / {__name__=commonPrefix+"suffix2"}`.
-* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions), [transform functions](#transform-functions) and [binary operators](https://prometheus.io/docs/prometheus/latest/querying/operators/#binary-operators).
+* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions), [transform functions](#transform-functions)
+  and [binary operators](https://prometheus.io/docs/prometheus/latest/querying/operators/#binary-operators).
   This modifier prevents from dropping metric names in function results. See [these docs](#keep_metric_names).
 
 ## keep_metric_names
@@ -155,14 +163,15 @@ Additional details:
   The interval between points is set as `step` query arg passed by Grafana to [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query).
 * If the given [series selector](https://docs.victoriametrics.com/keyConcepts.html#filtering) returns multiple time series,
   then rollups are calculated individually per each returned series.
-* If lookbehind window in square brackets is missing, then MetricsQL automatically sets the lookbehind window
-  to the interval between points on the graph (aka `step` query arg at [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query),
-  `$__interval` value from Grafana or `1i` duration in MetricsQL).
-  For example, `rate(http_requests_total)` is equivalent to `rate(http_requests_total[$__interval])` in Grafana.
-  It is also equivalent to `rate(http_requests_total[1i])`.
+* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
+  - To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query) or [/api/v1/query](https://docs.victoriametrics.com/keyconcepts/#instant-query)
+    for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
+    For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
+  - To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples)
+    for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
 * Every [series selector](https://docs.victoriametrics.com/keyConcepts.html#filtering) in MetricsQL must be wrapped into a rollup function.
   Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
-  is automatically converted to `default_rollup(foo{bar="baz"}[1i])` before performing the calculations.
+  is automatically converted to `default_rollup(foo{bar="baz"})` before performing the calculations.
 * If something other than [series selector](https://docs.victoriametrics.com/keyConcepts.html#filtering) is passed to rollup function,
   then the inner arg is automatically converted to a [subquery](#subqueries).
 * All the rollup functions accept optional `keep_metric_names` modifier. If it is set, then the function keeps metric names in results.
@@ -299,6 +308,11 @@ See also [increases_over_time](#increases_over_time).
 `default_rollup(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last raw sample value on the given lookbehind window `d`
 per each time series returned from the given [series_selector](https://docs.victoriametrics.com/keyConcepts.html#filtering).
 
+If the lookbehind window is skipped in square brackets, then it is automatically calculated as `max(step, scrape_interval)`, where `step` is the query arg value
+passed to [/api/v1/query_range](https://docs.victoriametrics.com/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/keyconcepts/#instant-query),
+while `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples) for the selected time series.
+This allows avoiding unexpected gaps on the graph when `step` is smaller than the `scrape_interval`.
+
 #### delta
 
 `delta(series_selector[d])` is a [rollup function](#rollup-functions), which calculates the difference between
@@ -622,6 +636,11 @@ Metric names are stripped from the resulting rollups. Add [keep_metric_names](#k
 over the given lookbehind window `d` per each time series returned from the given [series_selector](https://docs.victoriametrics.com/keyConcepts.html#filtering).
 It is expected that the `series_selector` returns time series of [counter type](https://docs.victoriametrics.com/keyConcepts.html#counter).
 
+If the lookbehind window is skipped in square brackets, then it is automatically calculated as `max(step, scrape_interval)`, where `step` is the query arg value
+passed to [/api/v1/query_range](https://docs.victoriametrics.com/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/keyconcepts/#instant-query),
+while `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples) for the selected time series.
+This allows avoiding unexpected gaps on the graph when `step` is smaller than the `scrape_interval`.
+
 Metric names are stripped from the resulting rollups. Add [keep_metric_names](#keep_metric_names) modifier in order to keep metric names.
 
 This function is supported by PromQL. See also [irate](#irate) and [rollup_rate](#rollup_rate).
@@ -920,7 +939,7 @@ Additional details:
 
 * If transform function is applied directly to a [series selector](https://docs.victoriametrics.com/keyConcepts.html#filtering),
   then the [default_rollup()](#default_rollup) function is automatically applied before calculating the transformations.
-  For example, `abs(temperature)` is implicitly transformed to `abs(default_rollup(temperature[1i]))`.
+  For example, `abs(temperature)` is implicitly transformed to `abs(default_rollup(temperature))`.
 * All the transform functions accept optional `keep_metric_names` modifier. If it is set,
   then the function doesn't drop metric names from the resulting time series. See [these docs](#keep_metric_names).
 
@@ -1580,7 +1599,7 @@ Additional details:
 
 * If label manipulation function is applied directly to a [series_selector](https://docs.victoriametrics.com/keyConcepts.html#filtering),
   then the [default_rollup()](#default_rollup) function is automatically applied before performing the label transformation.
-  For example, `alias(temperature, "foo")` is implicitly transformed to `alias(default_rollup(temperature[1i]), "foo")`.
+  For example, `alias(temperature, "foo")` is implicitly transformed to `alias(default_rollup(temperature), "foo")`.
 
 See also [implicit query conversions](#implicit-query-conversions).
 
@@ -1757,7 +1776,7 @@ Additional details:
   Multiple labels can be put in `by` and `without` modifiers.
 * If the aggregate function is applied directly to a [series_selector](https://docs.victoriametrics.com/keyConcepts.html#filtering),
   then the [default_rollup()](#default_rollup) function is automatically applied before calculating the aggregate.
-  For example, `count(up)` is implicitly transformed to `count(default_rollup(up[1i]))`.
+  For example, `count(up)` is implicitly transformed to `count(default_rollup(up))`.
 * Aggregate functions accept arbitrary number of args. For example, `avg(q1, q2, q3)` would return the average values for every point
   across time series returned by `q1`, `q2` and `q3`.
 * Aggregate functions support optional `limit N` suffix, which can be used for limiting the number of output groups.
@@ -2061,7 +2080,7 @@ See also [zscore_over_time](#zscore_over_time), [range_trim_zscore](#range_trim_
 MetricsQL supports and extends PromQL subqueries. See [this article](https://valyala.medium.com/prometheus-subqueries-in-victoriametrics-9b1492b720b3) for details.
 Any [rollup function](#rollup-functions) for something other than [series selector](https://docs.victoriametrics.com/keyConcepts.html#filtering) form a subquery.
 Nested rollup functions can be implicit thanks to the [implicit query conversions](#implicit-query-conversions).
-For example, `delta(sum(m))` is implicitly converted to `delta(sum(default_rollup(m[1i]))[1i:1i])`, so it becomes a subquery,
+For example, `delta(sum(m))` is implicitly converted to `delta(sum(default_rollup(m))[1i:1i])`, so it becomes a subquery,
 since it contains [default_rollup](#default_rollup) nested into [delta](#delta).
 
 VictoriaMetrics performs subqueries in the following way:
@@ -2076,21 +2095,23 @@ VictoriaMetrics performs subqueries in the following way:
 
 VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
 
-* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions),
-  then `[1i]` is automatically added there. The `[1i]` means one `step` value, which is passed
-  to [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query).
-  It is also known as `$__interval` in Grafana. For example, `rate(http_requests_count)` is automatically transformed to `rate(http_requests_count[1i])`.
+* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
+  - To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/keyConcepts.html#range-query) or [/api/v1/query](https://docs.victoriametrics.com/keyconcepts/#instant-query)
+    for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
+    For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
+  - To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/keyconcepts/#raw-samples)
+    for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
 * All the [series selectors](https://docs.victoriametrics.com/keyConcepts.html#filtering),
   which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.
   Examples:
-  * `foo` is transformed to `default_rollup(foo[1i])`
-  * `foo + bar` is transformed to `default_rollup(foo[1i]) + default_rollup(bar[1i])`
-  * `count(up)` is transformed to `count(default_rollup(up[1i]))`, because [count](#count) isn't a [rollup function](#rollup-functions) -
+  * `foo` is transformed to `default_rollup(foo)`
+  * `foo + bar` is transformed to `default_rollup(foo) + default_rollup(bar)`
+  * `count(up)` is transformed to `count(default_rollup(up))`, because [count](#count) isn't a [rollup function](#rollup-functions) -
     it is [aggregate function](#aggregate-functions)
-  * `abs(temperature)` is transformed to `abs(default_rollup(temperature[1i]))`, because [abs](#abs) isn't a [rollup function](#rollup-functions) -
+  * `abs(temperature)` is transformed to `abs(default_rollup(temperature))`, because [abs](#abs) isn't a [rollup function](#rollup-functions) -
     it is [transform function](#transform-functions)
 * If `step` in square brackets is missing inside [subquery](#subqueries), then `1i` step is automatically added there.
   For example, `avg_over_time(rate(http_requests_total[5m])[1h])` is automatically converted to `avg_over_time(rate(http_requests_total[5m])[1h:1i])`.
 * If something other than [series selector](https://docs.victoriametrics.com/keyConcepts.html#filtering)
   is passed to [rollup function](#rollup-functions), then a [subquery](#subqueries) with `1i` lookbehind window and `1i` step is automatically formed.
-  For example, `rate(sum(up))` is automatically converted to `rate((sum(default_rollup(up[1i])))[1i:1i])`.
+  For example, `rate(sum(up))` is automatically converted to `rate((sum(default_rollup(up)))[1i:1i])`.
diff --git a/docs/README.md b/docs/README.md
index 70f33b4c5..a494a42a9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -21,24 +21,27 @@ VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMet
 [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Snap packages](https://snapcraft.io/victoriametrics)
 and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). 
 
-The cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+Documentation for the cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
 
 Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow the 
 [quick start guide](https://docs.victoriametrics.com/Quick-Start.html) for a better experience.
 
-There is also a user-friendly database for logs - [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/).
-
 If you have questions about VictoriaMetrics, then feel free asking them in the [VictoriaMetrics community Slack chat](https://slack.victoriametrics.com/).
 
-[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. 
+[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics.
 See [features available in enterprise package](https://docs.victoriametrics.com/enterprise.html).
-Enterprise binaries can be downloaded and evaluated for free 
+Enterprise binaries can be downloaded and evaluated for free
 from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
 You can also [request a free trial license](https://victoriametrics.com/products/enterprise/trial/).
 
-VictoriaMetrics is developed at a fast pace, so it is recommended to check the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) periodically, and to perform [regular upgrades](#how-to-upgrade-victoriametrics).
+VictoriaMetrics is developed at a fast pace, so it is recommended to check the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) periodically,
+and to perform [regular upgrades](#how-to-upgrade-victoriametrics).
+
+[VictoriaMetrics enterprise](https://docs.victoriametrics.com/enterprise/) provides long-term support lines of releases (LTS releases) -
+see [these docs](https://docs.victoriametrics.com/LTS-releases.md).
 
-VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services. We apply strict security measures in everything we do. See our [Security page](https://victoriametrics.com/security/) for more details.
+VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services.
+We apply strict security measures in everything we do. See [Security page](https://victoriametrics.com/security/) for more details.
 
 ## Prominent features
 
@@ -105,6 +108,23 @@ VictoriaMetrics has the following prominent features:
 
 See [case studies for VictoriaMetrics](https://docs.victoriametrics.com/CaseStudies.html) and [various Articles about VictoriaMetrics](https://docs.victoriametrics.com/Articles.html).
 
+## Components
+
+VictoriaMetrics ecosystem contains the following components additionally to [single-node VictoriaMetrics](https://docs.victoriametrics.com/):
+
+- [vmagent](https://docs.victoriametrics.com/vmagent/) - lightweight agent for receiving metrics via [pull-based](https://docs.victoriametrics.com/vmagent/#how-to-collect-metrics-in-prometheus-format)
+  and [push-based](https://docs.victoriametrics.com/vmagent/#how-to-push-data-to-vmagent) protocols, transforming and sending them to the configured Prometheus-compatible
+  remote storage systems such as VictoriaMetrics.
+- [vmalert](https://docs.victoriametrics.com/vmalert/) - a service for processing Prometheus-compatible alerting and recording rules.
+- [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/) -  a tool for validating alerting and recording rules.
+- [vmauth](https://docs.victoriametrics.com/vmauth/) - authorization proxy and load balancer optimized for VictoriaMetrics products.
+- [vmgateway](https://docs.victoriametrics.com/vmgateway/) - auhtorization proxy with per-[tenant](https://docs.victoriametrics.com/cluster-victoriametrics/#multitenancy) rate limiting cababilities.
+- [vmctl](https://docs.victoriametrics.com/vmctl/) - a tool for migrating and copying data between different storage systems for metrics.
+- [vmbackup](https://docs.victoriametrics.com/vmbackup/), [vmrestore](https://docs.victoriametrics.com/vmrestore/) and [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager/) -
+  tools for creating backups and restoring from backups for VictoriaMetrics data.
+- `vminsert`, `vmselect` and `vmstorage` - components of [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+- [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/) - user-friendly cost-efficient database for logs.
+
 ## Operation
 
 ### Install
diff --git a/docs/Single-server-VictoriaMetrics.md b/docs/Single-server-VictoriaMetrics.md
index aaeb0c1aa..e06f3af79 100644
--- a/docs/Single-server-VictoriaMetrics.md
+++ b/docs/Single-server-VictoriaMetrics.md
@@ -29,24 +29,27 @@ VictoriaMetrics is available in [binary releases](https://github.com/VictoriaMet
 [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Snap packages](https://snapcraft.io/victoriametrics)
 and [source code](https://github.com/VictoriaMetrics/VictoriaMetrics). 
 
-The cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+Documentation for the cluster version of VictoriaMetrics is available [here](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
 
 Learn more about [key concepts](https://docs.victoriametrics.com/keyConcepts.html) of VictoriaMetrics and follow the 
 [quick start guide](https://docs.victoriametrics.com/Quick-Start.html) for a better experience.
 
-There is also a user-friendly database for logs - [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/).
-
 If you have questions about VictoriaMetrics, then feel free asking them in the [VictoriaMetrics community Slack chat](https://slack.victoriametrics.com/).
 
-[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics. 
+[Contact us](mailto:info@victoriametrics.com) if you need enterprise support for VictoriaMetrics.
 See [features available in enterprise package](https://docs.victoriametrics.com/enterprise.html).
-Enterprise binaries can be downloaded and evaluated for free 
+Enterprise binaries can be downloaded and evaluated for free
 from [the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
 You can also [request a free trial license](https://victoriametrics.com/products/enterprise/trial/).
 
-VictoriaMetrics is developed at a fast pace, so it is recommended to check the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) periodically, and to perform [regular upgrades](#how-to-upgrade-victoriametrics).
+VictoriaMetrics is developed at a fast pace, so it is recommended to check the [CHANGELOG](https://docs.victoriametrics.com/CHANGELOG.html) periodically,
+and to perform [regular upgrades](#how-to-upgrade-victoriametrics).
+
+[VictoriaMetrics enterprise](https://docs.victoriametrics.com/enterprise/) provides long-term support lines of releases (LTS releases) -
+see [these docs](https://docs.victoriametrics.com/LTS-releases.md).
 
-VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services. We apply strict security measures in everything we do. See our [Security page](https://victoriametrics.com/security/) for more details.
+VictoriaMetrics has achieved security certifications for Database Software Development and Software-Based Monitoring Services.
+We apply strict security measures in everything we do. See [Security page](https://victoriametrics.com/security/) for more details.
 
 ## Prominent features
 
@@ -113,6 +116,23 @@ VictoriaMetrics has the following prominent features:
 
 See [case studies for VictoriaMetrics](https://docs.victoriametrics.com/CaseStudies.html) and [various Articles about VictoriaMetrics](https://docs.victoriametrics.com/Articles.html).
 
+## Components
+
+VictoriaMetrics ecosystem contains the following components additionally to [single-node VictoriaMetrics](https://docs.victoriametrics.com/):
+
+- [vmagent](https://docs.victoriametrics.com/vmagent/) - lightweight agent for receiving metrics via [pull-based](https://docs.victoriametrics.com/vmagent/#how-to-collect-metrics-in-prometheus-format)
+  and [push-based](https://docs.victoriametrics.com/vmagent/#how-to-push-data-to-vmagent) protocols, transforming and sending them to the configured Prometheus-compatible
+  remote storage systems such as VictoriaMetrics.
+- [vmalert](https://docs.victoriametrics.com/vmalert/) - a service for processing Prometheus-compatible alerting and recording rules.
+- [vmalert-tool](https://docs.victoriametrics.com/vmalert-tool/) -  a tool for validating alerting and recording rules.
+- [vmauth](https://docs.victoriametrics.com/vmauth/) - authorization proxy and load balancer optimized for VictoriaMetrics products.
+- [vmgateway](https://docs.victoriametrics.com/vmgateway/) - auhtorization proxy with per-[tenant](https://docs.victoriametrics.com/cluster-victoriametrics/#multitenancy) rate limiting cababilities.
+- [vmctl](https://docs.victoriametrics.com/vmctl/) - a tool for migrating and copying data between different storage systems for metrics.
+- [vmbackup](https://docs.victoriametrics.com/vmbackup/), [vmrestore](https://docs.victoriametrics.com/vmrestore/) and [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager/) -
+  tools for creating backups and restoring from backups for VictoriaMetrics data.
+- `vminsert`, `vmselect` and `vmstorage` - components of [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html).
+- [VictoriaLogs](https://docs.victoriametrics.com/VictoriaLogs/) - user-friendly cost-efficient database for logs.
+
 ## Operation
 
 ### Install
diff --git a/docs/anomaly-detection/CHANGELOG.md b/docs/anomaly-detection/CHANGELOG.md
index d70469500..b41574c5f 100644
--- a/docs/anomaly-detection/CHANGELOG.md
+++ b/docs/anomaly-detection/CHANGELOG.md
@@ -17,6 +17,23 @@ Please find the changelog for VictoriaMetrics Anomaly Detection below.
 
 > **Important note: Users are strongly encouraged to upgrade to `vmanomaly` [v1.9.2](https://hub.docker.com/repository/docker/victoriametrics/vmanomaly/tags?page=1&ordering=name) or later versions for optimal performance and accuracy. <br><br> This recommendation is crucial for configurations with a low `infer_every` parameter [in your scheduler](https://docs.victoriametrics.com/anomaly-detection/components/scheduler/#parameters-1), and in scenarios where data exhibits significant high-order seasonality patterns (such as hourly or daily cycles). Previous versions from v1.5.1 to v1.8.0 were identified to contain a critical issue impacting model training, where models were inadvertently trained on limited data subsets, leading to suboptimal fits, affecting the accuracy of anomaly detection. <br><br> Upgrading to v1.9.2 addresses this issue, ensuring proper model training and enhanced reliability. For users utilizing Helm charts, it is recommended to upgrade to version [1.0.0](https://github.com/VictoriaMetrics/helm-charts/blob/master/charts/victoria-metrics-anomaly/CHANGELOG.md#100).**
 
+## v1.10.0
+Released: 2024-02-15
+- FEATURE: Multi-model support. Now users can specify multiple [model specs](https://docs.victoriametrics.com/anomaly-detection/components/models/) in a single config (via aliasing), as well as to reference what [queries from VmReader](https://docs.victoriametrics.com/anomaly-detection/components/reader/?highlight=queries#config-parameters) it should be run on.
+  - Introduction of `queries` arg in model spec:
+    - It allows the model to be executed only on a particular query subset from `reader` section. 
+    - Passing an empty list or not specifying this param implies that each model is run on results from **all** queries, which is a backward-compatible behavior.
+  - Please find more details in docs on [Model section](https://docs.victoriametrics.com/anomaly-detection/components/models/#queries)
+
+- DEPRECATION: slight refactor of a model config section 
+  - Now models are passed as a mapping of `model_alias: model_spec` under [model](https://docs.victoriametrics.com/anomaly-detection/components/models/) sections. Using old format (<= [1.9.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v192)) will produce warnings for now and will be removed in future versions.
+  - Please find more details in docs on [Model section](https://docs.victoriametrics.com/anomaly-detection/components/models/)
+- IMPROVEMENT: now logs from [`monitoring.pull`](https://docs.victoriametrics.com/anomaly-detection/components/monitoring/#monitoring-section-config-example) GET requests to `/metrics` endpoint are shown only in DEBUG mode
+- IMPROVEMENT: labelset for multivariate models is deduplicated and cleaned, resulting in better UX
+
+> **Note**: These updates support more flexible setup and effective resource management in service, as now it's not longer needed to spawn several instances of `vmanomaly` to split queries/models context across.
+
+
 ## v1.9.2
 Released: 2024-01-29
 - BUGFIX: now multivariate models (like [`IsolationForestMultivariateModel`](https://docs.victoriametrics.com/anomaly-detection/components/models/#isolation-foresthttpsenwikipediaorgwikiisolation_forest-multivariate)) are properly handled throughout fit/infer phases.
diff --git a/docs/anomaly-detection/Overview.md b/docs/anomaly-detection/Overview.md
index 11498959a..71e8f0bcd 100644
--- a/docs/anomaly-detection/Overview.md
+++ b/docs/anomaly-detection/Overview.md
@@ -162,10 +162,11 @@ scheduler:
   fit_every: "2h"
   fit_window: "14d"
 
-model:
-  class: "model.prophet.ProphetModel"
-  args:
-    interval_width: 0.98
+models:
+  prophet:  # or use a model alias of your choice here
+    class: "model.prophet.ProphetModel"
+    args:
+      interval_width: 0.98
 
 reader:
   datasource_url: [YOUR_DATASOURCE_URL] #Example: "http://victoriametrics:8428/"
@@ -217,7 +218,7 @@ This will expose metrics at `http://0.0.0.0:8080/metrics` page.
 To use *vmanomaly* you need to pull docker image:
 
 ```sh
-docker pull victoriametrics/vmanomaly:v1.9.2
+docker pull victoriametrics/vmanomaly:v1.10.0
 ```
 
 > Note: please check what is latest release in [CHANGELOG](/anomaly-detection/CHANGELOG.html)
@@ -227,7 +228,7 @@ docker pull victoriametrics/vmanomaly:v1.9.2
 You can put a tag on it for your convinience:
 
 ```sh
-docker image tag victoriametrics/vmanomaly:v1.9.2 vmanomaly
+docker image tag victoriametrics/vmanomaly:v1.10.0 vmanomaly
 ```
 Here is an example of how to run *vmanomaly* docker container with [license file](#licensing):
 
diff --git a/docs/anomaly-detection/components/models.md b/docs/anomaly-detection/components/models.md
index c2dffda18..a190f55b8 100644
--- a/docs/anomaly-detection/components/models.md
+++ b/docs/anomaly-detection/components/models.md
@@ -16,10 +16,149 @@ aliases:
 
 # Models
 
-This section describes `Model` component of VictoriaMetrics Anomaly Detection (or simply [`vmanomaly`](/anomaly-detection/overview.html)) and the guide of how to define a respective section of a config to launch the service.
+This section describes `Models` component of VictoriaMetrics Anomaly Detection (or simply [`vmanomaly`](/anomaly-detection/overview.html)) and the guide of how to define a respective section of a config to launch the service.
 vmanomaly includes various [built-in models](#built-in-models) and you can integrate your custom model with vmanomaly see [custom model](#custom-model-guide) 
 
 
+> **Note: Starting from [v1.10.0](/anomaly-detection/changelog#v1100) model section in config supports multiple models via aliasing. <br>Also, `vmanomaly` expects model section to be named `models`. Using old (flat) format with `model` key is deprecated and will be removed in future versions. Having `model` and `models` sections simultaneously in a config will result in only `models` being used:**
+
+```yaml
+models:
+  model_univariate_1:
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
+    queries: ["query_alias2"]  # referencing queries defined in `reader` section
+  model_multivariate_1:
+    class: "model.isolation_forest.IsolationForestMultivariateModel"
+    contamination: "auto"
+    args:
+      n_estimators: 100
+      # i.e. to assure reproducibility of produced results each time model is fit on the same input
+      random_state: 42
+    # if there is no explicit `queries` arg, then the model will be run on ALL queries found in reader section
+```  
+
+Old-style configs (< [1.10.0](/anomaly-detection/changelog#v1100) )
+
+```yaml
+model:
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
+    # no explicit `queries` arg is provided
+```
+
+will be **implicitly** converted to
+
+```yaml
+models:
+  default_model:  # default model alias, backward compatibility
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
+    # queries arg is created and propagated with all query aliases found in `queries` arg of `reader` section
+    queries: ["q1", "q2", "q3"]  # i.e., if your `queries` in `reader` section has exactly q1, q2, q3 aliases
+```
+
+
+## Common args
+
+From [1.10.0](/anomaly-detection/changelog#1100), **common args**, supported by *every model (and model type)* were introduced.
+
+### Queries
+
+Introduced in [1.10.0](/anomaly-detection/changelog#1100), as a part to support multi-model configs, `queries` arg is meant to define [queries from VmReader](https://docs.victoriametrics.com/anomaly-detection/components/reader/?highlight=queries#config-parameters) particular model should be run on (meaning, all the series returned by each of these queries will be used in such model for fitting and inferencing).
+
+`queries` arg is supported for all [the built-in](/anomaly-detection/components/models/#built-in-models) (as well as for [custom](/anomaly-detection/components/models/#custom-model-guide)) models.
+
+This arg is **backward compatible** - if there is no explicit `queries` arg, then the model, defined in a config, will be run on ALL queries found in reader section:
+
+```yaml
+models:
+  model_alias_1:
+    ...
+    # no explicit `queries` arg is provided
+```
+
+will be implicitly converted to
+
+```yaml
+models:
+  model_alias_1:
+    ...
+    # queries arg is created and propagated with all query aliases found in `queries` arg of `reader` section
+    queries: ["q1", "q2", "q3"]  # i.e., if your `queries` in `reader` section has exactly q1, q2, q3 aliases
+```
+
+## Model types
+
+There are **2 model types**, supported in `vmanomaly`, resulting in **4 possible combinations**:
+
+- [Univariate models](#univariate-models)
+- [Multivariate models](#multivariate-models)
+
+Each of these models can be
+- [rolling](#rolling-models)
+- [non-rolling](#non-rolling-models)
+
+### Univariate Models
+
+For a univariate type, **one separate model** is fit/used for inference per **each time series**, defined in its [queries](#queries) arg.
+
+For example, if you have some **univariate** model, defined to use 3 [MetricQL queries](https://docs.victoriametrics.com/metricsql/), each returning 5 time series, there will be 3*5=15 models created in total. Each such model produce **individual [output](#vmanomaly-output)** for each of time series. 
+
+If during an inference, you got a series having **new labelset** (not present in any of fitted models), the inference will be skipped until you get a model, trained particularly for such labelset during forthcoming re-fit step.
+
+**Implications:** Univariate models are a go-to default, when your queries returns **changing** amount of **individual** time series of **different** magnitude, [trend](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#trend) or [seasonality](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-1/#seasonality), so you won't be mixing incompatible data with different behavior within a single fit model (context isolation).
+
+**Examples:** [Prophet](#prophet), [Holt-Winters](#holt-winters)
+
+<!-- TODO: add schema -->
+
+### Multivariate Models
+
+For a multivariate type, **one shared model** is fit/used for inference on **all time series** simultaneously, defined in its [queries](#queries) arg. 
+
+For example, if you have some **multivariate** model to use 3 [MetricQL queries](https://docs.victoriametrics.com/metricsql/), each returning 5 time series, there will be one shared model created in total. Once fit, this model will expect **exactly 15 time series with exact same labelsets as an input**. This model will produce **one shared [output](#vmanomaly-output)**.
+
+If during an inference, you got a **different amount of series** or some series having a **new labelset** (not present in any of fitted models), the inference will be skipped until you get a model, trained particularly for such labelset during forthcoming re-fit step. 
+
+**Implications:** Multivariate models are a go-to default, when your queries returns **fixed** amount of **individual** time series (say, some aggregations), to be used for adding cross-series (and cross-query) context, useful for catching [collective anomalies](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/index.html#collective-anomalies) or [novelties](https://victoriametrics.com/blog/victoriametrics-anomaly-detection-handbook-chapter-2/index.html#novelties) (expanded to multi-input scenario). For example, you may set it up for anomaly detection of CPU usage in different modes (`idle`, `user`, `system`, etc.) and use its cross-dependencies to detect **unseen (in fit data)** behavior.
+
+**Examples:** [IsolationForest](#isolation-forest-multivariate)
+
+<!-- TODO: add schema -->
+
+### Rolling Models
+
+A rolling model is a model that, once trained, **cannot be (naturally) used to make inference on data, not seen during its fit phase**.
+
+An instance of rolling model is **simultaneously fit and used for inference** during its `infer` method call.
+
+As a result, such model instances are **not stored** between consecutive re-fit calls (defined by `fit_every` [arg](/anomaly-detection/components/scheduler/?highlight=fit_every#periodic-scheduler) in `PeriodicScheduler`), leading to **lower RAM** consumption.
+
+Such models put **more pressure** on your reader's source, i.e. if your model should be fit on large amount of data (say, 14 days with 1-minute resolution) and at the same time you have **frequent inference** (say, once per minute) on new chunks of data - that's because such models require (fit + infer) window of data to be fit first to be used later in each inference call.
+
+> **Note**: Rolling models require `fit_every` to be set equal to `infer_every` in your [PeriodicScheduler](/anomaly-detection/components/scheduler/?highlight=fit_every#periodic-scheduler).
+
+**Examples:** [RollingQuantile](#rolling-quantile)
+
+<!-- TODO: add schema -->
+
+### Non-Rolling Models
+
+Everything that is not classified as [rolling](#rolling-models). 
+
+Produced models can be explicitly used to **infer on data, not seen during its fit phase**, thus, it **doesn't require re-fit procedure**.
+
+Such models put **less pressure** on your reader's source, i.e. if you fit on large amount of data (say, 14 days with 1-minute resolution) but do it occasionally (say, once per day), at the same time you have **frequent inference**(say, once per minute) on new chunks of data
+
+> **Note**: However, it's still highly recommended, to keep your model up-to-date with tendencies found in your data as it evolves in time.
+
+Produced model instances are **stored in-memory** between consecutive re-fit calls (defined by `fit_every` [arg](/anomaly-detection/components/scheduler/?highlight=fit_every#periodic-scheduler) in `PeriodicScheduler`), leading to **higher RAM** consumption.
+
+**Examples:** [Prophet](#prophet)
+
+<!-- TODO: add schema -->
+
 ## Built-in Models 
 
 ### Overview
@@ -68,18 +207,19 @@ Depending on chosen `seasonality` parameter FB Prophet can return additional met
 *Config Example*
 
 ```yaml
-model:
-  class: "model.prophet.ProphetModel"
-  seasonalities:
-    - name: 'hourly'
-      period: 0.04166666666
-      fourier_order: 30
-  # Inner model args (key-value pairs) accepted by
-  # https://facebook.github.io/prophet/docs/quick_start.html#python-api
-  args:
-    # See https://facebook.github.io/prophet/docs/uncertainty_intervals.html
-    interval_width: 0.98
-    country_holidays: 'US'
+models:
+  your_desired_alias_for_a_model:
+    class: "model.prophet.ProphetModel"
+    seasonalities:
+      - name: 'hourly'
+        period: 0.04166666666
+        fourier_order: 30
+    # Inner model args (key-value pairs) accepted by
+    # https://facebook.github.io/prophet/docs/quick_start.html#python-api
+    args:
+      # See https://facebook.github.io/prophet/docs/uncertainty_intervals.html
+      interval_width: 0.98
+      country_holidays: 'US'
 ```
 
 
@@ -95,9 +235,10 @@ Resulting metrics of the model are described [here](#vmanomaly-output)
 
 
 ```yaml
-model:
-  class: "model.zscore.ZscoreModel"
-  z_threshold: 2.5
+models:
+  your_desired_alias_for_a_model:
+    class: "model.zscore.ZscoreModel"
+    z_threshold: 2.5
 ```
 
 
@@ -131,14 +272,15 @@ Used to compute "seasonal_periods" param for the model (e.g. '1D' or '1W').
 *Config Example*
 
 ```yaml
-model:
-  class: "model.holtwinters.HoltWinters"
-  seasonality: '1d'
-  frequency: '1h'
-  # Inner model args (key-value pairs) accepted by statsmodels.tsa.holtwinters.ExponentialSmoothing
-  args:
-    seasonal: 'add'
-    initialization_method: 'estimated'
+models:
+  your_desired_alias_for_a_model:
+    class: "model.holtwinters.HoltWinters"
+    seasonality: '1d'
+    frequency: '1h'
+    # Inner model args (key-value pairs) accepted by statsmodels.tsa.holtwinters.ExponentialSmoothing
+    args:
+      seasonal: 'add'
+      initialization_method: 'estimated'
 ```
 
 
@@ -156,9 +298,10 @@ The MAD model is a robust method for anomaly detection that is *less sensitive*
 
 
 ```yaml
-model:
-  class: "model.mad.MADModel"
-  threshold: 2.5
+models:
+  your_desired_alias_for_a_model:
+    class: "model.mad.MADModel"
+    threshold: 2.5
 ```
 
 
@@ -175,13 +318,13 @@ Resulting metrics of the model are described [here](#vmanomaly-output).
 *Config Example*
 
 ```yaml
-model:
-  class: "model.rolling_quantile.RollingQuantileModel"
-  quantile: 0.9
-  window_steps: 96
+models:
+  your_desired_alias_for_a_model:
+    class: "model.rolling_quantile.RollingQuantileModel"
+    quantile: 0.9
+    window_steps: 96
 ```
 
-
 Resulting metrics of the model are described [here](#vmanomaly-output).
 
 ### [Seasonal Trend Decomposition](https://en.wikipedia.org/wiki/Seasonal_adjustment)
@@ -198,9 +341,10 @@ Here we use Seasonal Decompose implementation from `statsmodels` [library](https
 
 
 ```yaml
-model:
-  class: "model.std.StdModel"
-  period: 2
+models:
+  your_desired_alias_for_a_model:
+    class: "model.std.StdModel"
+    period: 2
 ```
 
 
@@ -233,15 +377,16 @@ Here we use ARIMA implementation from `statsmodels` [library](https://www.statsm
 *Config Example*
 
 ```yaml
-model:
-  class: "model.arima.ArimaModel"
-  # ARIMA's (p,d,q) order
-  order: [1, 1, 0] 
-  z_threshold: 2.7
-  resample_freq: '1m'
-  # Inner model args (key-value pairs) accepted by statsmodels.tsa.arima.model.ARIMA
-  args:
-    trend: 'c'
+models:
+  your_desired_alias_for_a_model:
+    class: "model.arima.ArimaModel"
+    # ARIMA's (p,d,q) order
+    order: [1, 1, 0] 
+    z_threshold: 2.7
+    resample_freq: '1m'
+    # Inner model args (key-value pairs) accepted by statsmodels.tsa.arima.model.ARIMA
+    args:
+      trend: 'c'
 ```
 
 
@@ -264,14 +409,15 @@ Here we use Isolation Forest implementation from `scikit-learn` [library](https:
 
 
 ```yaml
-model:
-  # To use univariate model, substitute class argument with "model.isolation_forest.IsolationForestModel".
-  class: "model.isolation_forest.IsolationForestMultivariateModel"
-  contamination: "auto"
-  args:
-    n_estimators: 100
-    # i.e. to assure reproducibility of produced results each time model is fit on the same input
-    random_state: 42
+models:
+  your_desired_alias_for_a_model:
+    # To use univariate model, substitute class argument with "model.isolation_forest.IsolationForestModel".
+    class: "model.isolation_forest.IsolationForestMultivariateModel"
+    contamination: "auto"
+    args:
+      n_estimators: 100
+      # i.e. to assure reproducibility of produced results each time model is fit on the same input
+      random_state: 42
 ```
 
 
@@ -318,6 +464,8 @@ Here in this guide, we will
 
 ### 1. Custom model
 
+> **Note**: By default, each custom model is created as [**univariate**](#univariate-models) / [**non-rolling**](#non-rolling-models) model. If you want to override this behavior, define models inherited from `RollingModel` (to get a rolling model), or having `is_multivariate` class arg set to `True` (please refer to the code example below).
+
 We'll create `custom_model.py` file with `CustomModel` class that will inherit from vmanomaly `Model` base class.
 In the `CustomModel` class there should be three required methods - `__init__`, `fit` and `infer`:
 * `__init__` method should initiate parameters for the model.
@@ -327,7 +475,7 @@ In the `CustomModel` class there should be three required methods - `__init__`,
   super().__init__(**kwargs)
   ``` 
   to initialize the base class each model derives from
-* `fit` method should contain the model training process.
+* `fit` method should contain the model training process. Please be aware that for `RollingModel` defining `fit` method is not needed, as the whole fit/infer process should be defined completely in `infer` method.
 * `infer` should return Pandas.DataFrame object with model's inferences.
 
 For the sake of simplicity, the model in this example will return one of two values of `anomaly_score` - 0 or 1 depending on input parameter `percentage`.
@@ -340,6 +488,7 @@ import scipy.stats as st
 import logging
 
 from model.model import Model
+# from model.model import RollingModel  # inherit from it for your model to be of rolling type
 logger = logging.getLogger(__name__)
 
 
@@ -348,6 +497,10 @@ class CustomModel(Model):
     Custom model implementation.
     """
 
+    # by default, each `Model` will be created as a univariate one
+    # uncomment line below for it to be of multivariate type
+    # is_multivariate = True
+
     def __init__(self, percentage: float = 0.95, **kwargs):
         super().__init__(**kwargs)
         self.percentage = percentage
@@ -362,7 +515,6 @@ class CustomModel(Model):
         if self._std == 0.0:
             self._std = 1 / 65536
 
-
     def infer(self, df: pd.DataFrame) -> np.array:
         # Inference process:
         y = df['y']
@@ -373,7 +525,6 @@ class CustomModel(Model):
         df_pred['anomaly_score'] = df_pred['anomaly_score'].astype('int32', errors='ignore')
 
         return df_pred
-
 ```
 
 
@@ -381,8 +532,8 @@ class CustomModel(Model):
 ### 2. Configuration file
 
 Next, we need to create `config.yaml` file with VM Anomaly Detection configuration and model input parameters.
-In the config file `model` section we need to put our model class `model.custom.CustomModel` and all parameters used in `__init__` method.
-You can find out more about configuration parameters in vmanomaly docs.
+In the config file `models` section we need to put our model class `model.custom.CustomModel` and all parameters used in `__init__` method.
+You can find out more about configuration parameters in [vmanomaly config docs](/anomaly-detection/components/).
 
 
 ```yaml
@@ -391,11 +542,12 @@ scheduler:
   fit_every: "1m"
   fit_window: "1d"
 
-model:
-  # note: every custom model should implement this exact path, specified in `class` field
-  class: "model.model.CustomModel"
-  # custom model params are defined here
-  percentage: 0.9
+models:
+  your_desired_alias_for_a_model:
+    # note: every custom model should implement this exact path, specified in `class` field
+    class: "model.model.CustomModel"
+    # custom model params are defined here
+    percentage: 0.9
 
 reader:
   datasource_url: "http://localhost:8428/"
@@ -426,15 +578,13 @@ monitoring:
 ### 3. Running custom model
 Let's pull the docker image for vmanomaly:
 
-
 ```sh 
-docker pull us-docker.pkg.dev/victoriametrics-test/public/vmanomaly-trial:latest
+docker pull victoriametrics/vmanomaly:latest
 ```
 
-
 Now we can run the docker container putting as volumes both config and model file:
 
-**Note**: place the model file to `/model/custom.py` path when copying
+> **Note**: place the model file to `/model/custom.py` path when copying
 
 ```sh
 docker run -it \
@@ -442,11 +592,10 @@ docker run -it \
 -v [YOUR_LICENSE_FILE_PATH]:/license.txt \
 -v $(PWD)/custom_model.py:/vmanomaly/src/model/custom.py \
 -v $(PWD)/custom.yaml:/config.yaml \
-us-docker.pkg.dev/victoriametrics-test/public/vmanomaly-trial:latest /config.yaml \
+victoriametrics/vmanomaly:latest /config.yaml \
 --license-file=/license.txt
 ```
 
-
 Please find more detailed instructions (license, etc.) [here](/anomaly-detection/overview.html#run-vmanomaly-docker-container)
 
 
diff --git a/docs/anomaly-detection/components/monitoring.md b/docs/anomaly-detection/components/monitoring.md
index 2f9e239c1..ebb15ca9d 100644
--- a/docs/anomaly-detection/components/monitoring.md
+++ b/docs/anomaly-detection/components/monitoring.md
@@ -128,6 +128,8 @@ monitoring:
 ### Models Behaviour Metrics
 Label names [description](#labelnames)
 
+> **Note**: There is a new label key `model_alias` introduced in multi-model support [v1.10.0](/anomaly-detection/changelog/#v1100). This label key adjustment was made to preserve unique label set production during writing produced metrics back to VictoriaMetrics.
+
 <table>
     <thead>
         <tr>
@@ -142,25 +144,25 @@ Label names [description](#labelnames)
             <td><code>vmanomaly_model_runs</code></td>
             <td>Counter</td>
             <td>How many times models ran (per model)</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_model_run_duration_seconds</code></td>
             <td>Summary</td>
             <td>How much time (in seconds) model invocations took</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_model_datapoints_accepted</code></td>
             <td>Counter</td>
             <td>How many datapoints did models accept</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_model_datapoints_produced</code></td>
             <td>Counter</td>
             <td>How many datapoints were generated by models</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
         <tr>
             <td><code>vmanomaly_models_active</code></td>
@@ -172,7 +174,7 @@ Label names [description](#labelnames)
             <td><code>vmanomaly_model_runs_skipped</code></td>
             <td>Counter</td>
             <td>How many times a run was skipped (per model)</td>
-            <td><code>stage, query_key</code></td>
+            <td><code>stage, query_key, model_alias</code></td>
         </tr>
     </tbody>
 </table>
@@ -286,6 +288,8 @@ Label names [description](#labelnames)
 
 <code>query_key</code> - query alias from [`reader`](/anomaly-detection/components/reader.html) config section.
 
+<code>model_alias</code> - model alias from [`models`](/anomaly-detection/components/models.html) config section. **Introduced in [v1.10.0](/anomaly-detection/changelog/#v1100).**
+
 <code>url</code> - writer or reader url endpoint.
 
 <code>code</code> - response status code or `connection_error`, `timeout`.
diff --git a/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md b/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md
index 5c76ed461..ce5a2a51f 100644
--- a/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md
+++ b/docs/anomaly-detection/guides/guide-vmanomaly-vmalert.md
@@ -36,7 +36,7 @@ aliases:
 
 All the service parameters are defined in a config file.
 
-> **Note**: As of the time of writing, in the [1.9.2](https://docs.victoriametrics.com/anomaly-detection/changelog/#v192) release and earlier versions, each `vmanomaly` configuration file is limited to supporting only one model type. To utilize *different models* on your data, it is necessary to run multiple instances of the `vmanomaly` process. Each instance should operate with its own configuration file, differing in the `model` section.
+> **Note**: Starting from [1.10.0](https://docs.victoriametrics.com/anomaly-detection/changelog/#v1100), each `vmanomaly` configuration file can support more that one model type. To utilize *different models* on your data, it is no longer necessary to run multiple instances of the `vmanomaly` process. Please refer to [model](/anomaly-detection/models/) config section for more details.
 
 
 **vmanomaly** does the following:
@@ -117,7 +117,7 @@ The configuration file for `vmanomaly` comprises 4 essential sections:
 
 1. [`scheduler`](/anomaly-detection/components/scheduler.html) - This section determines the frequency of model inferences and training, including the time range for model training.
 
-2. [`model`](/anomaly-detection/components/models.html) - Here, you define specific parameters and configurations for the model being used for anomaly detection.
+2. [`models`](/anomaly-detection/components/models.html) - Here, you define specific parameters and configurations for the models being used for anomaly detection.
 
 3. [`reader`](/anomaly-detection/components/reader.html) - This section outlines the methodology for data reading, including the data source location.
 
@@ -132,7 +132,7 @@ Detailed parameters in each section:
   * `fit_every` - Sets the frequency for retraining the models. A higher frequency ensures more updated models but requires more CPU resources. If omitted, models are retrained in each `infer_every` cycle. Format is similar to `infer_every`.
   * `fit_window` - Defines the data interval for training the models. Longer intervals allow for capturing extensive historical behavior and better seasonal pattern detection but may slow down the model's response to permanent metric changes and increase resource consumption. A minimum of two full seasonal cycles is recommended. Example format: 3h for three hours of data.
 
-* `model`
+* `models`
   * `class` - Specifies the model to be used. Options include custom models ([guide here](/anomaly-detection/components/models.html#custom-model-guide)) or a selection from [built-in models](/anomaly-detection/components/models.html#built-in-models), such as the [Facebook Prophet](/anomaly-detection/components/models.html#prophet) (`model.prophet.ProphetModel`).
   * `args` - Model-specific parameters, formatted as a YAML dictionary in the `key: value` structure. Parameters available in [FB Prophet](https://facebook.github.io/prophet/docs/quick_start.html) can be used as an example.
 
@@ -152,10 +152,11 @@ scheduler:
   fit_every: "2m"
   fit_window: "3h"
 
-model:
-  class: "model.prophet.ProphetModel"
-  args:
-    interval_width: 0.98
+models:
+  prophet:
+    class: "model.prophet.ProphetModel"
+    args:
+      interval_width: 0.98
 
 reader:
   datasource_url: "http://victoriametrics:8428/"
diff --git a/docs/enterprise.md b/docs/enterprise.md
index 4dfeaf0ad..d972a7bf7 100644
--- a/docs/enterprise.md
+++ b/docs/enterprise.md
@@ -40,7 +40,7 @@ See [these docs](#running-victoriametrics-enterprise) for details on how to run
 VictoriaMetrics Enterprise includes [all the features of the community edition](https://docs.victoriametrics.com/#prominent-features),
 plus the following additional features:
 
-- Stable releases with long-term support, which contains important bugfixes. See, for example, [v1.97.x LTS release](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.97.1).
+- Stable releases with long-term support, which contains important bugfixes and security fixes. See [these docs](https://docs.victoriametrics.com/LTS-releases.html).
 - First-class consulting and technical support provided by the core VictoriaMetrics dev team.
 - [Monitoring of monitoring](https://victoriametrics.com/products/mom/) - this feature allows forecasting
   and preventing possible issues in VictoriaMetrics setups.
diff --git a/docs/sd_configs.md b/docs/sd_configs.md
index 9ed10bd76..389fb7dfd 100644
--- a/docs/sd_configs.md
+++ b/docs/sd_configs.md
@@ -1243,6 +1243,12 @@ scrape_configs:
     #
   - server: "http://localhost:5676"
 
+    # client_id is an optional client ID to send to Kuma Control Plane.
+    # The hostname of the server where vmagent runs is used if it isn't set.
+    # If the hostname is empty, then "vmagent" string is used as client_id.
+    #
+    # client_id: "..."
+
     # Additional HTTP API client options can be specified here.
     # See https://docs.victoriametrics.com/sd_configs.html#http-api-client-options
 ```
diff --git a/docs/vmctl.md b/docs/vmctl.md
index 034c189f6..135939237 100644
--- a/docs/vmctl.md
+++ b/docs/vmctl.md
@@ -280,7 +280,11 @@ foo_field2{tag1="value1", tag2="value2"} 40
 
 ### Configuration
 
-The configuration flags should contain self-explanatory descriptions.
+Run the following command to get all configuration options:
+```sh
+./vmctl influx --help
+```
+
 
 ### Filtering
 
@@ -421,7 +425,11 @@ So no data changes will be applied.
 
 ### Configuration
 
-The configuration flags should contain self-explanatory descriptions.
+Run the following command to get all configuration options:
+```sh
+./vmctl prometheus --help
+```
+
 
 ### Filtering
 
diff --git a/go.mod b/go.mod
index 33b94beb1..96828789c 100644
--- a/go.mod
+++ b/go.mod
@@ -8,8 +8,8 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.0
 	github.com/VictoriaMetrics/easyproto v0.1.4
 	github.com/VictoriaMetrics/fastcache v1.12.2
-	github.com/VictoriaMetrics/metrics v1.31.0
-	github.com/VictoriaMetrics/metricsql v0.72.1
+	github.com/VictoriaMetrics/metrics v1.32.0
+	github.com/VictoriaMetrics/metricsql v0.73.0
 	github.com/aws/aws-sdk-go-v2 v1.25.0
 	github.com/aws/aws-sdk-go-v2/config v1.27.0
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.0
diff --git a/go.sum b/go.sum
index 1070ae69a..8ea40ede8 100644
--- a/go.sum
+++ b/go.sum
@@ -69,10 +69,10 @@ github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo
 github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
 github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
 github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys=
-github.com/VictoriaMetrics/metrics v1.31.0 h1:X6+nBvAP0UB+GjR0Ht9hhQ3pjL1AN4b8dt9zFfzTsUo=
-github.com/VictoriaMetrics/metrics v1.31.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
-github.com/VictoriaMetrics/metricsql v0.72.1 h1:fLIHgzezXgD4NjY5ksF4lRkHILW88uI5Lz0Q+N2ucnY=
-github.com/VictoriaMetrics/metricsql v0.72.1/go.mod h1:k4UaP/+CjuZslIjd+kCigNG9TQmUqh5v0TP/nMEy90I=
+github.com/VictoriaMetrics/metrics v1.32.0 h1:r9JK2zndYv0TIxFXLEHwhQqRdnu8/O3cwJiCBX4vJCM=
+github.com/VictoriaMetrics/metrics v1.32.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
+github.com/VictoriaMetrics/metricsql v0.73.0 h1:MvYnUIZHWD+Kj+sKuBSI1asR1fw1BxQPGshs32C7FIk=
+github.com/VictoriaMetrics/metricsql v0.73.0/go.mod h1:k4UaP/+CjuZslIjd+kCigNG9TQmUqh5v0TP/nMEy90I=
 github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
 github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
diff --git a/lib/httputils/duration.go b/lib/httputils/duration.go
index 4fdd05c4a..f2a6df300 100644
--- a/lib/httputils/duration.go
+++ b/lib/httputils/duration.go
@@ -29,7 +29,7 @@ func GetDuration(r *http.Request, argKey string, defaultValue int64) (int64, err
 	}
 	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 0, fmt.Errorf("%s=%dms is out of allowed range [%dms ... %dms]", argKey, msecs, 1, int64(maxDurationMsecs))
 	}
 	return msecs, nil
 }
diff --git a/lib/logstorage/storage_search.go b/lib/logstorage/storage_search.go
index c728d9998..136e2b821 100644
--- a/lib/logstorage/storage_search.go
+++ b/lib/logstorage/storage_search.go
@@ -1,6 +1,7 @@
 package logstorage
 
 import (
+	"context"
 	"math"
 	"sort"
 	"sync"
@@ -43,7 +44,7 @@ type searchOptions struct {
 }
 
 // RunQuery runs the given q and calls processBlock for results
-func (s *Storage) RunQuery(tenantIDs []TenantID, q *Query, stopCh <-chan struct{}, processBlock func(columns []BlockColumn)) {
+func (s *Storage) RunQuery(tenantIDs []TenantID, q *Query, stopCh <-chan struct{}, processBlock func(columns []BlockColumn) bool) {
 	resultColumnNames := q.getResultColumnNames()
 	so := &genericSearchOptions{
 		tenantIDs:         tenantIDs,
@@ -51,7 +52,7 @@ func (s *Storage) RunQuery(tenantIDs []TenantID, q *Query, stopCh <-chan struct{
 		resultColumnNames: resultColumnNames,
 	}
 	workersCount := cgroup.AvailableCPUs()
-	s.search(workersCount, so, stopCh, func(workerID uint, br *blockResult) {
+	s.search(workersCount, so, stopCh, func(workerID uint, br *blockResult) bool {
 		brs := getBlockRows()
 		cs := brs.cs
 
@@ -61,10 +62,11 @@ func (s *Storage) RunQuery(tenantIDs []TenantID, q *Query, stopCh <-chan struct{
 				Values: br.getColumnValues(i),
 			})
 		}
-		processBlock(cs)
+		limitReached := processBlock(cs)
 
 		brs.cs = cs
 		putBlockRows(brs)
+		return limitReached
 	})
 }
 
@@ -118,7 +120,7 @@ const blockSearchWorksPerBatch = 64
 // searchResultFunc must process sr.
 //
 // The callback is called at the worker with the given workerID.
-type searchResultFunc func(workerID uint, br *blockResult)
+type searchResultFunc func(workerID uint, br *blockResult) bool
 
 // search searches for the matching rows according to so.
 //
@@ -128,19 +130,34 @@ func (s *Storage) search(workersCount int, so *genericSearchOptions, stopCh <-ch
 	var wg sync.WaitGroup
 	workCh := make(chan []*blockSearchWork, workersCount)
 	wg.Add(workersCount)
+
+	ctx, cancelFn := context.WithCancel(context.Background())
+
 	for i := 0; i < workersCount; i++ {
 		go func(workerID uint) {
+			defer wg.Done()
 			bs := getBlockSearch()
-			for bsws := range workCh {
-				for _, bsw := range bsws {
-					bs.search(bsw)
-					if bs.br.RowsCount() > 0 {
-						processBlockResult(workerID, &bs.br)
+			defer putBlockSearch(bs)
+			for {
+				select {
+				case bsws, ok := <-workCh:
+					if !ok {
+						return
 					}
+					for _, bsw := range bsws {
+						bs.search(bsw)
+						if bs.br.RowsCount() > 0 {
+							limitReached := processBlockResult(workerID, &bs.br)
+							if limitReached {
+								cancelFn()
+								return
+							}
+						}
+					}
+				case <-ctx.Done():
+					return
 				}
 			}
-			putBlockSearch(bs)
-			wg.Done()
 		}(uint(i))
 	}
 
@@ -178,6 +195,7 @@ func (s *Storage) search(workersCount int, so *genericSearchOptions, stopCh <-ch
 	// Wait until workers finish their work
 	close(workCh)
 	wg.Wait()
+	cancelFn()
 
 	// Decrement references to parts
 	for _, pw := range pws {
diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go
index 3158d0b24..dc4e6169d 100644
--- a/lib/promscrape/config.go
+++ b/lib/promscrape/config.go
@@ -296,6 +296,12 @@ type ScrapeConfig struct {
 	MetricRelabelConfigs []promrelabel.RelabelConfig `yaml:"metric_relabel_configs,omitempty"`
 	SampleLimit          int                         `yaml:"sample_limit,omitempty"`
 
+	// This silly option is needed for compatibility with Prometheus.
+	// vmagent was supporting disable_compression option since the beginning, while Prometheus developers
+	// decided adding enable_compression option in https://github.com/prometheus/prometheus/pull/13166
+	// That's why it needs to be supported too :(
+	EnableCompression *bool `yaml:"enable_compression,omitempty"`
+
 	AzureSDConfigs        []azure.SDConfig        `yaml:"azure_sd_configs,omitempty"`
 	ConsulSDConfigs       []consul.SDConfig       `yaml:"consul_sd_configs,omitempty"`
 	ConsulAgentSDConfigs  []consulagent.SDConfig  `yaml:"consulagent_sd_configs,omitempty"`
@@ -879,6 +885,10 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf
 	if sc.SeriesLimit != nil {
 		seriesLimit = *sc.SeriesLimit
 	}
+	disableCompression := sc.DisableCompression
+	if sc.EnableCompression != nil {
+		disableCompression = !*sc.EnableCompression
+	}
 	swc := &scrapeWorkConfig{
 		scrapeInterval:       scrapeInterval,
 		scrapeIntervalString: scrapeInterval.String(),
@@ -898,7 +908,7 @@ func getScrapeWorkConfig(sc *ScrapeConfig, baseDir string, globalCfg *GlobalConf
 		relabelConfigs:       relabelConfigs,
 		metricRelabelConfigs: metricRelabelConfigs,
 		sampleLimit:          sc.SampleLimit,
-		disableCompression:   sc.DisableCompression,
+		disableCompression:   disableCompression,
 		disableKeepAlive:     sc.DisableKeepAlive,
 		streamParse:          sc.StreamParse,
 		scrapeAlignInterval:  sc.ScrapeAlignInterval.Duration(),
diff --git a/lib/promscrape/discovery/kuma/api.go b/lib/promscrape/discovery/kuma/api.go
index 26b62f081..fa27ad6d1 100644
--- a/lib/promscrape/discovery/kuma/api.go
+++ b/lib/promscrape/discovery/kuma/api.go
@@ -8,6 +8,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"os"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -23,8 +24,9 @@ import (
 var configMap = discoveryutils.NewConfigMap()
 
 type apiConfig struct {
-	client  *discoveryutils.Client
-	apiPath string
+	client   *discoveryutils.Client
+	clientID string
+	apiPath  string
 
 	// labels contains the latest discovered labels.
 	labels atomic.Pointer[[]*promutils.Labels]
@@ -65,9 +67,18 @@ func newAPIConfig(sdc *SDConfig, baseDir string) (*apiConfig, error) {
 		return nil, fmt.Errorf("cannot create HTTP client for %q: %w", apiServer, err)
 	}
 
+	clientID := sdc.ClientID
+	if clientID == "" {
+		clientID, _ = os.Hostname()
+		if clientID == "" {
+			clientID = "vmagent"
+		}
+	}
+
 	cfg := &apiConfig{
-		client:  client,
-		apiPath: apiPath,
+		client:   client,
+		clientID: clientID,
+		apiPath:  apiPath,
 
 		fetchErrors: metrics.GetOrCreateCounter(fmt.Sprintf(`promscrape_discovery_kuma_errors_total{type="fetch",url=%q}`, sdc.Server)),
 		parseErrors: metrics.GetOrCreateCounter(fmt.Sprintf(`promscrape_discovery_kuma_errors_total{type="parse",url=%q}`, sdc.Server)),
@@ -142,7 +153,7 @@ func (cfg *apiConfig) updateTargetsLabels(ctx context.Context) error {
 	dReq := &discoveryRequest{
 		VersionInfo: cfg.latestVersion,
 		Node: discoveryRequestNode{
-			ID: "vmagent",
+			ID: cfg.clientID,
 		},
 		TypeURL:       "type.googleapis.com/kuma.observability.v1.MonitoringAssignment",
 		ResponseNonce: cfg.latestNonce,
diff --git a/lib/promscrape/discovery/kuma/kuma.go b/lib/promscrape/discovery/kuma/kuma.go
index c5bd82490..11ba4fc75 100644
--- a/lib/promscrape/discovery/kuma/kuma.go
+++ b/lib/promscrape/discovery/kuma/kuma.go
@@ -19,7 +19,8 @@ var SDCheckInterval = flag.Duration("promscrape.kumaSDCheckInterval", 30*time.Se
 //
 // See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#kuma_sd_config
 type SDConfig struct {
-	Server string `yaml:"server"`
+	Server   string `yaml:"server"`
+	ClientID string `yaml:"client_id,omitempty"`
 
 	HTTPClientConfig  promauth.HTTPClientConfig  `yaml:",inline"`
 	ProxyURL          *proxy.URL                 `yaml:"proxy_url,omitempty"`
diff --git a/vendor/github.com/VictoriaMetrics/metrics/gauge.go b/vendor/github.com/VictoriaMetrics/metrics/gauge.go
index 9bbbce21b..3573e1445 100644
--- a/vendor/github.com/VictoriaMetrics/metrics/gauge.go
+++ b/vendor/github.com/VictoriaMetrics/metrics/gauge.go
@@ -17,7 +17,7 @@ import (
 //   - foo{bar="baz",aaa="b"}
 //
 // f must be safe for concurrent calls.
-// if f is nil, then it is expected that the gauge value is changed via Gauge.Set() call.
+// if f is nil, then it is expected that the gauge value is changed via Set(), Inc(), Dec() and Add() calls.
 //
 // The returned gauge is safe to use from concurrent goroutines.
 //
@@ -55,6 +55,38 @@ func (g *Gauge) Set(v float64) {
 	atomic.StoreUint64(&g.valueBits, n)
 }
 
+// Inc increments g by 1.
+//
+// The g must be created with nil callback in order to be able to call this function.
+func (g *Gauge) Inc() {
+	g.Add(1)
+}
+
+// Dec decrements g by 1.
+//
+// The g must be created with nil callback in order to be able to call this function.
+func (g *Gauge) Dec() {
+	g.Add(-1)
+}
+
+// Add adds fAdd to g. fAdd may be positive and negative.
+//
+// The g must be created with nil callback in order to be able to call this function.
+func (g *Gauge) Add(fAdd float64) {
+	if g.f != nil {
+		panic(fmt.Errorf("cannot call Set on gauge created with non-nil callback"))
+	}
+	for {
+		n := atomic.LoadUint64(&g.valueBits)
+		f := math.Float64frombits(n)
+		fNew := f + fAdd
+		nNew := math.Float64bits(fNew)
+		if atomic.CompareAndSwapUint64(&g.valueBits, n, nNew) {
+			break
+		}
+	}
+}
+
 func (g *Gauge) marshalTo(prefix string, w io.Writer) {
 	v := g.Get()
 	if float64(int64(v)) == v {
diff --git a/vendor/github.com/VictoriaMetrics/metricsql/optimizer.go b/vendor/github.com/VictoriaMetrics/metricsql/optimizer.go
index 507e93160..54302a2df 100644
--- a/vendor/github.com/VictoriaMetrics/metricsql/optimizer.go
+++ b/vendor/github.com/VictoriaMetrics/metricsql/optimizer.go
@@ -82,14 +82,24 @@ func getCommonLabelFilters(e Expr) []LabelFilter {
 	case *RollupExpr:
 		return getCommonLabelFilters(t.Expr)
 	case *FuncExpr:
-		if strings.ToLower(t.Name) == "label_set" {
+		switch strings.ToLower(t.Name) {
+		case "label_set":
 			return getCommonLabelFiltersForLabelSet(t.Args)
+		case "label_replace", "label_join", "label_map", "label_match", "label_mismatch", "label_transform":
+			return getCommonLabelFiltersForLabelReplace(t.Args)
+		case "label_copy", "label_move":
+			return getCommonLabelFiltersForLabelCopy(t.Args)
+		case "label_del", "label_uppercase", "label_lowercase", "labels_equal":
+			return getCommonLabelFiltersForLabelDel(t.Args)
+		case "label_keep":
+			return getCommonLabelFiltersForLabelKeep(t.Args)
+		default:
+			arg := getFuncArgForOptimization(t.Name, t.Args)
+			if arg == nil {
+				return nil
+			}
+			return getCommonLabelFilters(arg)
 		}
-		arg := getFuncArgForOptimization(t.Name, t.Args)
-		if arg == nil {
-			return nil
-		}
-		return getCommonLabelFilters(arg)
 	case *AggrFuncExpr:
 		args := t.Args
 		if len(args) > 0 && canAcceptMultipleArgsForAggrFunc(t.Name) {
@@ -164,6 +174,49 @@ func getCommonLabelFilters(e Expr) []LabelFilter {
 	}
 }
 
+func getCommonLabelFiltersForLabelKeep(args []Expr) []LabelFilter {
+	if len(args) == 0 {
+		return nil
+	}
+	lfs := getCommonLabelFilters(args[0])
+	lfs = keepLabelFiltersForLabelNames(lfs, args[1:])
+	return lfs
+}
+
+func getCommonLabelFiltersForLabelDel(args []Expr) []LabelFilter {
+	if len(args) == 0 {
+		return nil
+	}
+	lfs := getCommonLabelFilters(args[0])
+	lfs = dropLabelFiltersForLabelNames(lfs, args[1:])
+	return lfs
+}
+
+func getCommonLabelFiltersForLabelCopy(args []Expr) []LabelFilter {
+	if len(args) == 0 {
+		return nil
+	}
+	lfs := getCommonLabelFilters(args[0])
+	args = args[1:]
+	var labelNames []Expr
+	for i := 0; i < len(args); i += 2 {
+		if i+1 >= len(args) {
+			return nil
+		}
+		labelNames = append(labelNames, args[i+1])
+	}
+	lfs = dropLabelFiltersForLabelNames(lfs, labelNames)
+	return lfs
+}
+
+func getCommonLabelFiltersForLabelReplace(args []Expr) []LabelFilter {
+	if len(args) < 2 {
+		return nil
+	}
+	lfs := getCommonLabelFilters(args[0])
+	return dropLabelFiltersForLabelName(lfs, args[1])
+}
+
 func getCommonLabelFiltersForLabelSet(args []Expr) []LabelFilter {
 	if len(args) == 0 {
 		return nil
@@ -190,13 +243,8 @@ func getCommonLabelFiltersForLabelSet(args []Expr) []LabelFilter {
 			continue
 		}
 
-		lfsDst := lfs[:0]
-		for _, lf := range lfs {
-			if lf.Label != seLabelName.S {
-				lfsDst = append(lfsDst, lf)
-			}
-		}
-		lfs = append(lfsDst, LabelFilter{
+		lfs = dropLabelFiltersForLabelName(lfs, labelName)
+		lfs = append(lfs, LabelFilter{
 			Label: seLabelName.S,
 			Value: seLabelValue.S,
 		})
@@ -288,11 +336,18 @@ func pushdownBinaryOpFiltersInplace(e Expr, lfs []LabelFilter) {
 	case *RollupExpr:
 		pushdownBinaryOpFiltersInplace(t.Expr, lfs)
 	case *FuncExpr:
-		if strings.ToLower(t.Name) == "label_set" && len(t.Args) > 0 {
-			arg := t.Args[0]
-			lfs = getPushdownLabelFiltersForLabelSet(t.Args[1:], lfs)
-			pushdownBinaryOpFiltersInplace(arg, lfs)
-		} else {
+		switch strings.ToLower(t.Name) {
+		case "label_set":
+			pushdownLabelFiltersForLabelSet(t.Args, lfs)
+		case "label_replace", "label_join", "label_map", "label_match", "label_mismatch", "label_transform":
+			pushdownLabelFiltersForLabelReplace(t.Args, lfs)
+		case "label_copy", "label_move":
+			pushdownLabelFiltersForLabelCopy(t.Args, lfs)
+		case "label_del", "label_uppercase", "label_lowercase", "labels_equal":
+			pushdownLabelFiltersForLabelDel(t.Args, lfs)
+		case "label_keep":
+			pushdownLabelFiltersForLabelKeep(t.Args, lfs)
+		default:
 			arg := getFuncArgForOptimization(t.Name, t.Args)
 			if arg != nil {
 				pushdownBinaryOpFiltersInplace(arg, lfs)
@@ -318,24 +373,59 @@ func pushdownBinaryOpFiltersInplace(e Expr, lfs []LabelFilter) {
 	}
 }
 
-func getPushdownLabelFiltersForLabelSet(args []Expr, lfs []LabelFilter) []LabelFilter {
-	m := make(map[string]struct{})
+func pushdownLabelFiltersForLabelKeep(args []Expr, lfs []LabelFilter) {
+	if len(args) == 0 {
+		return
+	}
+	lfs = keepLabelFiltersForLabelNames(lfs, args[1:])
+	pushdownBinaryOpFiltersInplace(args[0], lfs)
+}
+
+func pushdownLabelFiltersForLabelDel(args []Expr, lfs []LabelFilter) {
+	if len(args) == 0 {
+		return
+	}
+	lfs = dropLabelFiltersForLabelNames(lfs, args[1:])
+	pushdownBinaryOpFiltersInplace(args[0], lfs)
+}
+
+func pushdownLabelFiltersForLabelCopy(args []Expr, lfs []LabelFilter) {
+	if len(args) == 0 {
+		return
+	}
+	arg := args[0]
+	args = args[1:]
+	var labelNames []Expr
 	for i := 0; i < len(args); i += 2 {
-		labelName := args[i]
-		seLabelName, ok := labelName.(*StringExpr)
-		if !ok {
-			return nil
+		if i+1 >= len(args) {
+			return
 		}
-		m[seLabelName.S] = struct{}{}
+		labelNames = append(labelNames, args[i+1])
 	}
+	lfs = dropLabelFiltersForLabelNames(lfs, labelNames)
+	pushdownBinaryOpFiltersInplace(arg, lfs)
+}
 
-	var lfsDst []LabelFilter
-	for _, lf := range lfs {
-		if _, ok := m[lf.Label]; !ok {
-			lfsDst = append(lfsDst, lf)
-		}
+func pushdownLabelFiltersForLabelReplace(args []Expr, lfs []LabelFilter) {
+	if len(args) < 2 {
+		return
 	}
-	return lfsDst
+	lfs = dropLabelFiltersForLabelName(lfs, args[1])
+	pushdownBinaryOpFiltersInplace(args[0], lfs)
+}
+
+func pushdownLabelFiltersForLabelSet(args []Expr, lfs []LabelFilter) {
+	if len(args) == 0 {
+		return
+	}
+	arg := args[0]
+	args = args[1:]
+	var labelNames []Expr
+	for i := 0; i < len(args); i += 2 {
+		labelNames = append(labelNames, args[i])
+	}
+	lfs = dropLabelFiltersForLabelNames(lfs, labelNames)
+	pushdownBinaryOpFiltersInplace(arg, lfs)
 }
 
 func intersectLabelFilters(lfsA, lfsB []LabelFilter) []LabelFilter {
@@ -354,6 +444,48 @@ func intersectLabelFilters(lfsA, lfsB []LabelFilter) []LabelFilter {
 	return lfs
 }
 
+func keepLabelFiltersForLabelNames(lfs []LabelFilter, labelNames []Expr) []LabelFilter {
+	m := make(map[string]struct{}, len(labelNames))
+	for _, labelName := range labelNames {
+		seLabelName, ok := labelName.(*StringExpr)
+		if !ok {
+			return nil
+		}
+		m[seLabelName.S] = struct{}{}
+	}
+
+	var lfsDst []LabelFilter
+	for _, lf := range lfs {
+		if _, ok := m[lf.Label]; ok {
+			lfsDst = append(lfsDst, lf)
+		}
+	}
+
+	return lfsDst
+}
+
+func dropLabelFiltersForLabelNames(lfs []LabelFilter, labelNames []Expr) []LabelFilter {
+	for _, labelName := range labelNames {
+		lfs = dropLabelFiltersForLabelName(lfs, labelName)
+	}
+	return lfs
+}
+
+func dropLabelFiltersForLabelName(lfs []LabelFilter, labelName Expr) []LabelFilter {
+	seLabelName, ok := labelName.(*StringExpr)
+	if !ok {
+		return nil
+	}
+
+	lfsDst := make([]LabelFilter, 0, len(lfs))
+	for _, lf := range lfs {
+		if lf.Label != seLabelName.S {
+			lfsDst = append(lfsDst, lf)
+		}
+	}
+	return lfsDst
+}
+
 func unionLabelFilters(lfsA, lfsB []LabelFilter) []LabelFilter {
 	if len(lfsA) == 0 {
 		return lfsB
@@ -498,7 +630,13 @@ func getRollupArgIdxForOptimization(funcName string, args []Expr) int {
 
 func getTransformArgIdxForOptimization(funcName string, args []Expr) int {
 	switch strings.ToLower(funcName) {
-	case "", "absent", "scalar", "union", "vector", "range_normalize":
+	case "label_copy", "label_del", "label_join", "label_keep", "label_lowercase", "label_map",
+		"label_match", "label_mismatch", "label_move", "label_replace", "label_set", "label_transform",
+		"label_uppercase", "labels_equal":
+		panic(fmt.Errorf("BUG: unexpected funcName passed to getTransformArgIdxForOptimization: %s", funcName))
+	case "drop_common_labels", "range_normalize":
+		return -1
+	case "", "absent", "scalar", "union", "vector":
 		return -1
 	case "end", "now", "pi", "ru", "start", "step", "time":
 		return -1
@@ -509,9 +647,8 @@ func getTransformArgIdxForOptimization(funcName string, args []Expr) int {
 		return 1
 	case "histogram_quantiles":
 		return len(args) - 1
-	case "drop_common_labels", "label_copy", "label_del", "label_graphite_group", "label_join", "label_keep", "label_lowercase",
-		"label_map", "label_move", "label_replace", "label_set", "label_transform", "label_uppercase":
-		return -1
+	case "label_graphite_group":
+		return 0
 	default:
 		return 0
 	}
diff --git a/vendor/github.com/VictoriaMetrics/metricsql/parser.go b/vendor/github.com/VictoriaMetrics/metricsql/parser.go
index 8458e5f71..ddd39c839 100644
--- a/vendor/github.com/VictoriaMetrics/metricsql/parser.go
+++ b/vendor/github.com/VictoriaMetrics/metricsql/parser.go
@@ -13,6 +13,26 @@ import (
 //
 // MetricsQL is backwards-compatible with PromQL.
 func Parse(s string) (Expr, error) {
+	// Parse s
+	e, err := parseInternal(s)
+	if err != nil {
+		return nil, err
+	}
+
+	// Expand `WITH` expressions.
+	was := getDefaultWithArgExprs()
+	if e, err = expandWithExpr(was, e); err != nil {
+		return nil, fmt.Errorf(`cannot expand WITH expressions: %s`, err)
+	}
+	e = removeParensExpr(e)
+	e = simplifyConstants(e)
+	if err := checkSupportedFunctions(e); err != nil {
+		return nil, err
+	}
+	return e, nil
+}
+
+func parseInternal(s string) (Expr, error) {
 	var p parser
 	p.lex.Init(s)
 	if err := p.lex.Next(); err != nil {
@@ -25,15 +45,6 @@ func Parse(s string) (Expr, error) {
 	if !isEOF(p.lex.Token) {
 		return nil, fmt.Errorf(`unparsed data left: %q`, p.lex.Context())
 	}
-	was := getDefaultWithArgExprs()
-	if e, err = expandWithExpr(was, e); err != nil {
-		return nil, fmt.Errorf(`cannot expand WITH expressions: %s`, err)
-	}
-	e = removeParensExpr(e)
-	e = simplifyConstants(e)
-	if err := checkSupportedFunctions(e); err != nil {
-		return nil, err
-	}
 	return e, nil
 }
 
@@ -104,36 +115,33 @@ func mustParseWithArgExpr(s string) *withArgExpr {
 
 // removeParensExpr removes parensExpr for (Expr) case.
 func removeParensExpr(e Expr) Expr {
-	if re, ok := e.(*RollupExpr); ok {
-		re.Expr = removeParensExpr(re.Expr)
-		if re.At != nil {
-			re.At = removeParensExpr(re.At)
+	switch t := e.(type) {
+	case *RollupExpr:
+		t.Expr = removeParensExpr(t.Expr)
+		if t.At != nil {
+			t.At = removeParensExpr(t.At)
 		}
-		return re
-	}
-	if be, ok := e.(*BinaryOpExpr); ok {
-		be.Left = removeParensExpr(be.Left)
-		be.Right = removeParensExpr(be.Right)
-		return be
-	}
-	if ae, ok := e.(*AggrFuncExpr); ok {
-		for i, arg := range ae.Args {
-			ae.Args[i] = removeParensExpr(arg)
+		return t
+	case *BinaryOpExpr:
+		t.Left = removeParensExpr(t.Left)
+		t.Right = removeParensExpr(t.Right)
+		return t
+	case *AggrFuncExpr:
+		for i, arg := range t.Args {
+			t.Args[i] = removeParensExpr(arg)
 		}
-		return ae
-	}
-	if fe, ok := e.(*FuncExpr); ok {
-		for i, arg := range fe.Args {
-			fe.Args[i] = removeParensExpr(arg)
+		return t
+	case *FuncExpr:
+		for i, arg := range t.Args {
+			t.Args[i] = removeParensExpr(arg)
 		}
-		return fe
-	}
-	if pe, ok := e.(*parensExpr); ok {
-		args := *pe
+		return t
+	case *parensExpr:
+		args := *t
 		for i, arg := range args {
 			args[i] = removeParensExpr(arg)
 		}
-		if len(*pe) == 1 {
+		if len(*t) == 1 {
 			return args[0]
 		}
 		// Treat parensExpr as a function with empty name, i.e. union()
@@ -142,38 +150,43 @@ func removeParensExpr(e Expr) Expr {
 			Args: args,
 		}
 		return fe
+	case *withExpr:
+		for _, arg := range t.Was {
+			arg.Expr = removeParensExpr(arg.Expr)
+		}
+		t.Expr = removeParensExpr(t.Expr)
+		return t
+	default:
+		return e
 	}
-	return e
 }
 
 func simplifyConstants(e Expr) Expr {
-	if re, ok := e.(*RollupExpr); ok {
-		re.Expr = simplifyConstants(re.Expr)
-		if re.At != nil {
-			re.At = simplifyConstants(re.At)
-		}
-		return re
-	}
-	if ae, ok := e.(*AggrFuncExpr); ok {
-		simplifyConstantsInplace(ae.Args)
-		return ae
-	}
-	if fe, ok := e.(*FuncExpr); ok {
-		simplifyConstantsInplace(fe.Args)
-		return fe
-	}
-	if pe, ok := e.(*parensExpr); ok {
-		if len(*pe) == 1 {
-			return simplifyConstants((*pe)[0])
+	switch t := e.(type) {
+	case *withExpr:
+		panic(fmt.Errorf("BUG: withExpr shouldn't be passed to simplifyConstants"))
+	case *parensExpr:
+		panic(fmt.Errorf("BUG: parensExpr shouldn't be passed to simplifyConstants"))
+	case *RollupExpr:
+		t.Expr = simplifyConstants(t.Expr)
+		if t.At != nil {
+			t.At = simplifyConstants(t.At)
 		}
-		simplifyConstantsInplace(*pe)
-		return pe
-	}
-	be, ok := e.(*BinaryOpExpr)
-	if !ok {
+		return t
+	case *AggrFuncExpr:
+		simplifyConstantsInplace(t.Args)
+		return t
+	case *FuncExpr:
+		simplifyConstantsInplace(t.Args)
+		return t
+	case *BinaryOpExpr:
+		return simplifyConstantsInBinaryExpr(t)
+	default:
 		return e
 	}
+}
 
+func simplifyConstantsInBinaryExpr(be *BinaryOpExpr) Expr {
 	be.Left = simplifyConstants(be.Left)
 	be.Right = simplifyConstants(be.Right)
 
@@ -202,7 +215,7 @@ func simplifyConstants(e Expr) Expr {
 		return be
 	}
 	// Perform string comparisons.
-	ok = false
+	ok := false
 	switch be.Op {
 	case "==":
 		ok = lse.S == rse.S
@@ -1715,7 +1728,13 @@ type StringExpr struct {
 // AppendString appends string representation of se to dst and returns the result.
 func (se *StringExpr) AppendString(dst []byte) []byte {
 	if len(se.tokens) > 0 {
-		panic(fmt.Errorf("BUG: StringExpr=%q must be already parsed with expandWithExpr()", se.tokens))
+		for i, token := range se.tokens {
+			dst = append(dst, token...)
+			if i+1 < len(se.tokens) {
+				dst = append(dst, '+')
+			}
+		}
+		return dst
 	}
 	return strconv.AppendQuote(dst, se.S)
 }
@@ -2182,15 +2201,24 @@ type MetricExpr struct {
 }
 
 func appendLabelFilterss(dst []byte, lfss [][]*labelFilterExpr) []byte {
+	offset := 0
+	metricName := getMetricNameFromLabelFilterss(lfss)
+	if metricName != "" {
+		offset = 1
+		dst = appendEscapedIdent(dst, metricName)
+	}
+	if isOnlyMetricNameInLabelFilterss(lfss) {
+		return dst
+	}
+
 	dst = append(dst, '{')
 	for i, lfs := range lfss {
-		for j, lf := range lfs {
-			dst = lf.AppendString(dst)
-			if j+1 < len(lfs) {
-				dst = append(dst, ',')
-			}
+		lfs = lfs[offset:]
+		if len(lfs) == 0 {
+			continue
 		}
-		if i+1 < len(lfss) {
+		dst = appendLabelFilterExprs(dst, lfs)
+		if i+1 < len(lfss) && len(lfss[i+1]) > offset {
 			dst = append(dst, " or "...)
 		}
 	}
@@ -2198,11 +2226,59 @@ func appendLabelFilterss(dst []byte, lfss [][]*labelFilterExpr) []byte {
 	return dst
 }
 
+func appendLabelFilterExprs(dst []byte, lfs []*labelFilterExpr) []byte {
+	for i, lf := range lfs {
+		dst = lf.AppendString(dst)
+		if i+1 < len(lfs) {
+			dst = append(dst, ',')
+		}
+	}
+	return dst
+}
+
+func isOnlyMetricNameInLabelFilterss(lfss [][]*labelFilterExpr) bool {
+	if getMetricNameFromLabelFilterss(lfss) == "" {
+		return false
+	}
+	for _, lfs := range lfss {
+		if len(lfs) > 1 {
+			return false
+		}
+	}
+	return true
+}
+
+func getMetricNameFromLabelFilterss(lfss [][]*labelFilterExpr) string {
+	if len(lfss) == 0 || len(lfss[0]) == 0 || lfss[0][0].Label != "__name__" || len(lfss[0][0].Value.tokens) != 1 {
+		return ""
+	}
+	metricName := mustExtractMetricNameFromToken(lfss[0][0].Value.tokens[0])
+	for _, lfs := range lfss[1:] {
+		if len(lfs) == 0 || lfs[0].Label != "__name__" || len(lfs[0].Value.tokens) != 1 {
+			return ""
+		}
+		metricNameLocal := mustExtractMetricNameFromToken(lfs[0].Value.tokens[0])
+		if metricNameLocal != metricName {
+			return ""
+		}
+	}
+	return metricName
+}
+
+func mustExtractMetricNameFromToken(token string) string {
+	metricName, err := extractStringValue(token)
+	if err != nil {
+		panic(fmt.Errorf("BUG: cannot obtain metric name: %w", err))
+	}
+	return metricName
+}
+
 // AppendString appends string representation of me to dst and returns the result.
 func (me *MetricExpr) AppendString(dst []byte) []byte {
 	if len(me.labelFilterss) > 0 {
 		return appendLabelFilterss(dst, me.labelFilterss)
 	}
+
 	lfss := me.LabelFilterss
 	if len(lfss) == 0 {
 		dst = append(dst, "{}"...)
@@ -2214,15 +2290,19 @@ func (me *MetricExpr) AppendString(dst []byte) []byte {
 		offset = 1
 		dst = appendEscapedIdent(dst, metricName)
 	}
-	if len(lfss) == 1 && len(lfss[0]) == offset {
+	if me.isOnlyMetricName() {
 		return dst
 	}
 	dst = append(dst, '{')
-	lfs := lfss[0]
-	dst = appendLabelFilters(dst, lfs[offset:])
-	for _, lfs := range lfss[1:] {
-		dst = append(dst, " or "...)
-		dst = appendLabelFilters(dst, lfs[offset:])
+	for i, lfs := range lfss {
+		lfs = lfs[offset:]
+		if len(lfs) == 0 {
+			continue
+		}
+		dst = appendLabelFilters(dst, lfs)
+		if i+1 < len(lfss) && len(lfss[i+1]) > offset {
+			dst = append(dst, " or "...)
+		}
 	}
 	dst = append(dst, '}')
 	return dst
diff --git a/vendor/github.com/VictoriaMetrics/metricsql/prettifier.go b/vendor/github.com/VictoriaMetrics/metricsql/prettifier.go
index 4aad090b8..01999650a 100644
--- a/vendor/github.com/VictoriaMetrics/metricsql/prettifier.go
+++ b/vendor/github.com/VictoriaMetrics/metricsql/prettifier.go
@@ -2,10 +2,11 @@ package metricsql
 
 // Prettify returns prettified representation of MetricsQL query q.
 func Prettify(q string) (string, error) {
-	e, err := Parse(q)
+	e, err := parseInternal(q)
 	if err != nil {
 		return "", err
 	}
+	e = removeParensExpr(e)
 	b := appendPrettifiedExpr(nil, e, 0, false)
 	return string(b), nil
 }
@@ -28,11 +29,11 @@ func appendPrettifiedExpr(dst []byte, e Expr, indent int, needParens bool) []byt
 		dst = append(dst, ')')
 	}
 	if len(dst)-dstLen <= maxPrettifiedLineLen {
-		// There is no need in splitting the e string representation, since its' length doesn't exceed.
+		// There is no need in splitting the e string representation, since its' length doesn't exceed maxPrettifiedLineLen.
 		return dst
 	}
 
-	// The e string representation exceeds maxPrettifiedLineLen. Split it into multiple lines
+	// The e string representation exceeds maxPrettifiedLineLen. Split it into multiple lines.
 	dst = dst[:dstLen]
 	if needParens {
 		dst = appendIndent(dst, indent)
@@ -40,6 +41,37 @@ func appendPrettifiedExpr(dst []byte, e Expr, indent int, needParens bool) []byt
 		indent++
 	}
 	switch t := e.(type) {
+	case *withExpr:
+		// Put every WITH expression on a separate line
+		dst = appendIndent(dst, indent)
+		dst = append(dst, "WITH (\n"...)
+		indent++
+		for _, wa := range t.Was {
+			dst = appendPrettifiedExpr(dst, wa, indent, false)
+			dst = append(dst, ",\n"...)
+		}
+		indent--
+		dst = appendIndent(dst, indent)
+		dst = append(dst, ")\n"...)
+		dst = appendPrettifiedExpr(dst, t.Expr, indent, false)
+	case *withArgExpr:
+		// Wrap long withArgExpr into `(...)`
+		dst = appendIndent(dst, indent)
+		dst = appendEscapedIdent(dst, t.Name)
+		if len(t.Args) > 0 {
+			dst = append(dst, '(')
+			dst = appendEscapedIdent(dst, t.Args[0])
+			for _, arg := range t.Args[1:] {
+				dst = append(dst, ", "...)
+				dst = appendEscapedIdent(dst, arg)
+			}
+			dst = append(dst, ')')
+		}
+		dst = append(dst, " = (\n"...)
+		dst = appendPrettifiedExpr(dst, t.Expr, indent+1, false)
+		dst = append(dst, '\n')
+		dst = appendIndent(dst, indent)
+		dst = append(dst, ')')
 	case *BinaryOpExpr:
 		// Split:
 		//
@@ -125,20 +157,24 @@ func appendPrettifiedExpr(dst []byte, e Expr, indent int, needParens bool) []byt
 		//       or
 		//     filtersN
 		//   }
+		lfss := t.labelFilterss
 		offset := 0
-		metricName := t.getMetricName()
+		metricName := getMetricNameFromLabelFilterss(lfss)
 		if metricName != "" {
 			offset = 1
 		}
 		dst = appendIndent(dst, indent)
 		dst = appendEscapedIdent(dst, metricName)
-		if !t.isOnlyMetricName() {
+		if !isOnlyMetricNameInLabelFilterss(lfss) {
 			dst = append(dst, "{\n"...)
-			lfss := t.LabelFilterss
 			for i, lfs := range lfss {
-				dst = appendPrettifiedLabelFilters(dst, indent+1, lfs[offset:])
+				lfs = lfs[offset:]
+				if len(lfs) == 0 {
+					continue
+				}
+				dst = appendPrettifiedLabelFilters(dst, indent+1, lfs)
 				dst = append(dst, '\n')
-				if i+1 < len(lfss) {
+				if i+1 < len(lfss) && len(lfss[i+1]) > offset {
 					dst = appendIndent(dst, indent+2)
 					dst = append(dst, "or\n"...)
 				}
@@ -173,12 +209,12 @@ func appendPrettifiedFuncArgs(dst []byte, indent int, args []Expr) []byte {
 	return dst
 }
 
-func appendPrettifiedLabelFilters(dst []byte, indent int, lfs []LabelFilter) []byte {
+func appendPrettifiedLabelFilters(dst []byte, indent int, lfs []*labelFilterExpr) []byte {
 	dstLen := len(dst)
 
 	// Try marshaling lfs into a single line
 	dst = appendIndent(dst, indent)
-	dst = appendLabelFilters(dst, lfs)
+	dst = appendLabelFilterExprs(dst, lfs)
 	if len(dst)-dstLen <= maxPrettifiedLineLen {
 		return dst
 	}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 168e5e10e..6ea7db837 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -99,10 +99,10 @@ github.com/VictoriaMetrics/easyproto
 # github.com/VictoriaMetrics/fastcache v1.12.2
 ## explicit; go 1.13
 github.com/VictoriaMetrics/fastcache
-# github.com/VictoriaMetrics/metrics v1.31.0
+# github.com/VictoriaMetrics/metrics v1.32.0
 ## explicit; go 1.17
 github.com/VictoriaMetrics/metrics
-# github.com/VictoriaMetrics/metricsql v0.72.1
+# github.com/VictoriaMetrics/metricsql v0.73.0
 ## explicit; go 1.13
 github.com/VictoriaMetrics/metricsql
 github.com/VictoriaMetrics/metricsql/binaryop


Test patch:
diff --git a/app/vmctl/utils/time_test.go b/app/vmctl/time_test.go
similarity index 98%
rename from app/vmctl/utils/time_test.go
rename to app/vmctl/time_test.go
index fde14e1e2..989162078 100644
--- a/app/vmctl/utils/time_test.go
+++ b/app/vmctl/time_test.go
@@ -1,4 +1,4 @@
-package utils
+package main
 
 import (
 	"testing"
@@ -165,7 +165,7 @@ func TestGetTime(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := GetTime(tt.s)
+			got, err := parseTime(tt.s)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/lib/logstorage/filters_test.go b/lib/logstorage/filters_test.go
index cf7d6e782..a5d017ee4 100644
--- a/lib/logstorage/filters_test.go
+++ b/lib/logstorage/filters_test.go
@@ -9226,7 +9226,7 @@ func testFilterMatchForStorage(t *testing.T, s *Storage, tenantID TenantID, f fi
 		resultColumnNames: []string{resultColumnName},
 	}
 	workersCount := 3
-	s.search(workersCount, so, nil, func(workerID uint, br *blockResult) {
+	s.search(workersCount, so, nil, func(workerID uint, br *blockResult) bool {
 		// Verify tenantID
 		if !br.streamID.tenantID.equal(&tenantID) {
 			t.Fatalf("unexpected tenantID in blockResult; got %s; want %s", &br.streamID.tenantID, &tenantID)
@@ -9248,6 +9248,7 @@ func testFilterMatchForStorage(t *testing.T, s *Storage, tenantID TenantID, f fi
 		if !reflect.DeepEqual(br.timestamps, expectedTimestamps) {
 			t.Fatalf("unexpected timestamps;\ngot\n%d\nwant\n%d", br.timestamps, expectedTimestamps)
 		}
+		return false
 	})
 }
 
diff --git a/lib/logstorage/storage_search_test.go b/lib/logstorage/storage_search_test.go
index 98b8f902a..9a0440d82 100644
--- a/lib/logstorage/storage_search_test.go
+++ b/lib/logstorage/storage_search_test.go
@@ -84,7 +84,7 @@ func TestStorageRunQuery(t *testing.T) {
 			AccountID: 0,
 			ProjectID: 0,
 		}
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		tenantIDs := []TenantID{tenantID}
@@ -96,7 +96,7 @@ func TestStorageRunQuery(t *testing.T) {
 			AccountID: 1,
 			ProjectID: 11,
 		}
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		tenantIDs := []TenantID{tenantID}
@@ -111,7 +111,7 @@ func TestStorageRunQuery(t *testing.T) {
 			}
 			expectedTenantID := tenantID.String()
 			rowsCount := uint32(0)
-			processBlock := func(columns []BlockColumn) {
+			processBlock := func(columns []BlockColumn) bool {
 				hasTenantIDColumn := false
 				var columnNames []string
 				for _, c := range columns {
@@ -132,6 +132,7 @@ func TestStorageRunQuery(t *testing.T) {
 					panic(fmt.Errorf("missing tenant.id column among columns: %q", columnNames))
 				}
 				atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+				return false
 			}
 			tenantIDs := []TenantID{tenantID}
 			s.RunQuery(tenantIDs, q, nil, processBlock)
@@ -145,8 +146,9 @@ func TestStorageRunQuery(t *testing.T) {
 	t.Run("matching-multiple-tenant-ids", func(t *testing.T) {
 		q := mustParseQuery(`"log message"`)
 		rowsCount := uint32(0)
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+			return false
 		}
 		s.RunQuery(allTenantIDs, q, nil, processBlock)
 
@@ -158,8 +160,9 @@ func TestStorageRunQuery(t *testing.T) {
 	t.Run("matching-in-filter", func(t *testing.T) {
 		q := mustParseQuery(`source-file:in(foobar,/foo/bar/baz)`)
 		rowsCount := uint32(0)
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+			return false
 		}
 		s.RunQuery(allTenantIDs, q, nil, processBlock)
 
@@ -170,7 +173,7 @@ func TestStorageRunQuery(t *testing.T) {
 	})
 	t.Run("stream-filter-mismatch", func(t *testing.T) {
 		q := mustParseQuery(`_stream:{job="foobar",instance=~"host-.+:2345"} log`)
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		s.RunQuery(allTenantIDs, q, nil, processBlock)
@@ -184,7 +187,7 @@ func TestStorageRunQuery(t *testing.T) {
 			}
 			expectedStreamID := fmt.Sprintf("stream_id=%d", i)
 			rowsCount := uint32(0)
-			processBlock := func(columns []BlockColumn) {
+			processBlock := func(columns []BlockColumn) bool {
 				hasStreamIDColumn := false
 				var columnNames []string
 				for _, c := range columns {
@@ -205,6 +208,7 @@ func TestStorageRunQuery(t *testing.T) {
 					panic(fmt.Errorf("missing stream-id column among columns: %q", columnNames))
 				}
 				atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+				return false
 			}
 			tenantIDs := []TenantID{tenantID}
 			s.RunQuery(tenantIDs, q, nil, processBlock)
@@ -222,8 +226,9 @@ func TestStorageRunQuery(t *testing.T) {
 			ProjectID: 11,
 		}
 		rowsCount := uint32(0)
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+			return false
 		}
 		tenantIDs := []TenantID{tenantID}
 		s.RunQuery(tenantIDs, q, nil, processBlock)
@@ -242,8 +247,9 @@ func TestStorageRunQuery(t *testing.T) {
 			ProjectID: 11,
 		}
 		rowsCount := uint32(0)
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+			return false
 		}
 		tenantIDs := []TenantID{tenantID}
 		s.RunQuery(tenantIDs, q, nil, processBlock)
@@ -262,8 +268,9 @@ func TestStorageRunQuery(t *testing.T) {
 			ProjectID: 11,
 		}
 		rowsCount := uint32(0)
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			atomic.AddUint32(&rowsCount, uint32(len(columns[0].Values)))
+			return false
 		}
 		tenantIDs := []TenantID{tenantID}
 		s.RunQuery(tenantIDs, q, nil, processBlock)
@@ -281,7 +288,7 @@ func TestStorageRunQuery(t *testing.T) {
 			AccountID: 1,
 			ProjectID: 11,
 		}
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		tenantIDs := []TenantID{tenantID}
@@ -295,7 +302,7 @@ func TestStorageRunQuery(t *testing.T) {
 			AccountID: 1,
 			ProjectID: 11,
 		}
-		processBlock := func(columns []BlockColumn) {
+		processBlock := func(columns []BlockColumn) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		tenantIDs := []TenantID{tenantID}
@@ -405,7 +412,7 @@ func TestStorageSearch(t *testing.T) {
 			filter:            f,
 			resultColumnNames: []string{"_msg"},
 		}
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		s.search(workersCount, so, nil, processBlock)
@@ -423,7 +430,7 @@ func TestStorageSearch(t *testing.T) {
 			filter:            f,
 			resultColumnNames: []string{"_msg"},
 		}
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		s.search(workersCount, so, nil, processBlock)
@@ -441,7 +448,7 @@ func TestStorageSearch(t *testing.T) {
 			filter:            f,
 			resultColumnNames: []string{"_msg"},
 		}
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		s.search(workersCount, so, nil, processBlock)
@@ -461,11 +468,12 @@ func TestStorageSearch(t *testing.T) {
 				resultColumnNames: []string{"_msg"},
 			}
 			rowsCount := uint32(0)
-			processBlock := func(workerID uint, br *blockResult) {
+			processBlock := func(workerID uint, br *blockResult) bool {
 				if !br.streamID.tenantID.equal(&tenantID) {
 					panic(fmt.Errorf("unexpected tenantID; got %s; want %s", &br.streamID.tenantID, &tenantID))
 				}
 				atomic.AddUint32(&rowsCount, uint32(br.RowsCount()))
+				return false
 			}
 			s.search(workersCount, so, nil, processBlock)
 
@@ -485,8 +493,9 @@ func TestStorageSearch(t *testing.T) {
 			resultColumnNames: []string{"_msg"},
 		}
 		rowsCount := uint32(0)
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			atomic.AddUint32(&rowsCount, uint32(br.RowsCount()))
+			return false
 		}
 		s.search(workersCount, so, nil, processBlock)
 
@@ -505,7 +514,7 @@ func TestStorageSearch(t *testing.T) {
 			filter:            f,
 			resultColumnNames: []string{"_msg"},
 		}
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		s.search(workersCount, so, nil, processBlock)
@@ -526,11 +535,12 @@ func TestStorageSearch(t *testing.T) {
 				resultColumnNames: []string{"_msg"},
 			}
 			rowsCount := uint32(0)
-			processBlock := func(workerID uint, br *blockResult) {
+			processBlock := func(workerID uint, br *blockResult) bool {
 				if !br.streamID.tenantID.equal(&tenantID) {
 					panic(fmt.Errorf("unexpected tenantID; got %s; want %s", &br.streamID.tenantID, &tenantID))
 				}
 				atomic.AddUint32(&rowsCount, uint32(br.RowsCount()))
+				return false
 			}
 			s.search(workersCount, so, nil, processBlock)
 
@@ -555,11 +565,12 @@ func TestStorageSearch(t *testing.T) {
 			resultColumnNames: []string{"_msg"},
 		}
 		rowsCount := uint32(0)
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			if !br.streamID.tenantID.equal(&tenantID) {
 				panic(fmt.Errorf("unexpected tenantID; got %s; want %s", &br.streamID.tenantID, &tenantID))
 			}
 			atomic.AddUint32(&rowsCount, uint32(br.RowsCount()))
+			return false
 		}
 		s.search(workersCount, so, nil, processBlock)
 
@@ -592,11 +603,12 @@ func TestStorageSearch(t *testing.T) {
 			resultColumnNames: []string{"_msg"},
 		}
 		rowsCount := uint32(0)
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			if !br.streamID.tenantID.equal(&tenantID) {
 				panic(fmt.Errorf("unexpected tenantID; got %s; want %s", &br.streamID.tenantID, &tenantID))
 			}
 			atomic.AddUint32(&rowsCount, uint32(br.RowsCount()))
+			return false
 		}
 		s.search(workersCount, so, nil, processBlock)
 
@@ -620,8 +632,9 @@ func TestStorageSearch(t *testing.T) {
 			resultColumnNames: []string{"_msg"},
 		}
 		rowsCount := uint32(0)
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			atomic.AddUint32(&rowsCount, uint32(br.RowsCount()))
+			return false
 		}
 		s.search(workersCount, so, nil, processBlock)
 
@@ -644,7 +657,7 @@ func TestStorageSearch(t *testing.T) {
 			filter:            f,
 			resultColumnNames: []string{"_msg"},
 		}
-		processBlock := func(workerID uint, br *blockResult) {
+		processBlock := func(workerID uint, br *blockResult) bool {
 			panic(fmt.Errorf("unexpected match"))
 		}
 		s.search(workersCount, so, nil, processBlock)
diff --git a/lib/promscrape/config_test.go b/lib/promscrape/config_test.go
index 950e455d2..437d5e27e 100644
--- a/lib/promscrape/config_test.go
+++ b/lib/promscrape/config_test.go
@@ -1147,6 +1147,7 @@ scrape_configs:
 	f(`
 scrape_configs:
 - job_name: path wo slash
+  enable_compression: false
   static_configs: 
   - targets: ["foo.bar:1234"]
   relabel_configs:
@@ -1161,7 +1162,8 @@ scrape_configs:
 				"instance": "foo.bar:1234",
 				"job":      "path wo slash",
 			}),
-			jobNameOriginal: "path wo slash",
+			DisableCompression: true,
+			jobNameOriginal:    "path wo slash",
 		},
 	})
 	f(`