Repository: VictoriaMetrics/VictoriaMetrics
Pull request #4639 (2023-07-14 12:16:45)

vmalert: allow to blackhole alerting notifications
Currently when -notifier.url and -notifier.config are not optional when rule are given. This enhancement allows make it optional using flag -notifier.blackhole.

As part of the enhancement, -notifier.url, -notifier.config, -notifier.blackhole are made mutually exclusive.
When notifier.blackhole is set to true, then a no-op notifier is added so that vmalert sends all the notifications to this no-op notifier. 

As part of this no-op notifier a metric with addr label as blackhole is added to know how many alerts are being sent. 

Fixes: 4122

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) Report
Patch coverage: **`84.37`**% and project coverage change: **`-0.02`** :warning:
> Comparison is base [(`79c4281`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/79c42814cfd2be6b52b5efdbeab7134e702ebe71?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 62.00% compared to head [(`c26765d`)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) 61.99%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4639      +/-   ##
==========================================
- Coverage   62.00%   61.99%   -0.02%     
==========================================
  Files         373      374       +1     
  Lines       69233    69222      -11     
==========================================
- Hits        42926    42911      -15     
- Misses      24024    24038      +14     
+ Partials     2283     2273      -10     
```


| [Impacted Files](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics) | Coverage Δ | |
|---|---|---|
| [app/vmalert/notifier/notifier\_blackhole.go](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3ZtYWxlcnQvbm90aWZpZXIvbm90aWZpZXJfYmxhY2tob2xlLmdv) | `70.58% <70.58%> (ø)` | |
| [app/vmalert/manager.go](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3ZtYWxlcnQvbWFuYWdlci5nbw==) | `79.64% <100.00%> (ø)` | |
| [app/vmalert/notifier/init.go](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics#diff-YXBwL3ZtYWxlcnQvbm90aWZpZXIvaW5pdC5nbw==) | `71.13% <100.00%> (+4.46%)` | :arrow_up: |

... and [14 files with indirect coverage changes](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/4639?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=VictoriaMetrics).

venkatbvc: @hagen1778 code coverage of the patch is 100%, but project has reduced. Do i need to add more tests for other code?
venkatbvc: @hagen1778 @Haleygo 
Added some tests to improve coverage.  Can you please review and trigger checks
venkatbvc: @hagen1778 @Haleygo  Fixed the lint issue. Please trigger the checks.

Patch:
diff --git a/app/vmalert/README.md b/app/vmalert/README.md
index 95923203c..48705e94c 100644
--- a/app/vmalert/README.md
+++ b/app/vmalert/README.md
@@ -1088,6 +1088,8 @@ The shortlist of configuration flags is the following:
   -notifier.url array
      Prometheus Alertmanager URL, e.g. http://127.0.0.1:9093. List all Alertmanager URLs if it runs in the cluster mode to ensure high availability.
      Supports an array of values separated by comma or specified via multiple flags.
+  -notifier.blackhole bool
+       Whether to blackhole alerting notifications. Enable this flag if you want vmalert to evaluate alerting rules without sending any notifications to external receivers (eg. alertmanager). `-notifier.url`, `-notifier.config` and `-notifier.blackhole` are mutually exclusive.
   -pprofAuthKey string
      Auth key for /debug/pprof/* endpoints. It must be passed via authKey query arg. It overrides httpAuth.* settings
   -promscrape.consul.waitTime duration
diff --git a/app/vmalert/manager.go b/app/vmalert/manager.go
index 25fdfbe92..26e909ddf 100644
--- a/app/vmalert/manager.go
+++ b/app/vmalert/manager.go
@@ -120,7 +120,7 @@ func (m *manager) update(ctx context.Context, groupsCfg []config.Group, restore
 		return fmt.Errorf("config contains recording rules but `-remoteWrite.url` isn't set")
 	}
 	if arPresent && m.notifiers == nil {
-		return fmt.Errorf("config contains alerting rules but neither `-notifier.url` nor `-notifier.config` aren't set")
+		return fmt.Errorf("config contains alerting rules but neither `-notifier.url` nor `-notifier.config` nor `-notifier.blackhole` aren't set")
 	}
 
 	type updateItem struct {
diff --git a/app/vmalert/notifier/init.go b/app/vmalert/notifier/init.go
index 8157b7c9a..82a88b30c 100644
--- a/app/vmalert/notifier/init.go
+++ b/app/vmalert/notifier/init.go
@@ -19,6 +19,7 @@ var (
 
 	addrs = flagutil.NewArrayString("notifier.url", "Prometheus Alertmanager URL, e.g. http://127.0.0.1:9093. "+
 		"List all Alertmanager URLs if it runs in the cluster mode to ensure high availability.")
+	blackHole = flag.Bool("notifier.blackhole", false, "Don't send any notifications to anywhere. -notifier.url and -notifier.blackhole and -notifier.config are mutually exclusive")
 
 	basicAuthUsername     = flagutil.NewArrayString("notifier.basicAuth.username", "Optional basic auth username for -notifier.url")
 	basicAuthPassword     = flagutil.NewArrayString("notifier.basicAuth.password", "Optional basic auth password for -notifier.url")
@@ -90,6 +91,17 @@ func Init(gen AlertURLGenerator, extLabels map[string]string, extURL string) (fu
 
 	templates.UpdateWithFuncs(templates.FuncsWithExternalURL(eu))
 
+	if *blackHole {
+		if len(*addrs) > 0 || *configPath != "" {
+			return nil, fmt.Errorf("only one of -notifier.blackhole, -notifier.url and -notifier.config flags must be specified")
+		}
+
+		staticNotifiersFn = func() []Notifier {
+			return []Notifier{NewBlackHoleNotifier()}
+		}
+		return staticNotifiersFn, nil
+	}
+
 	if *configPath == "" && len(*addrs) == 0 {
 		return nil, nil
 	}
diff --git a/app/vmalert/notifier/notifier_blackhole.go b/app/vmalert/notifier/notifier_blackhole.go
new file mode 100644
index 000000000..961919b19
--- /dev/null
+++ b/app/vmalert/notifier/notifier_blackhole.go
@@ -0,0 +1,35 @@
+package notifier
+
+import "context"
+
+// BlackHoleNotifier is used when no notifications needs to be sent
+type BlackHoleNotifier struct {
+	addr    string
+	metrics *metrics
+}
+
+// Send will not send any notifications. Only increase the alerts sent number.
+func (bh *BlackHoleNotifier) Send(_ context.Context, alerts []Alert, _ map[string]string) error { //nolint:revive
+	bh.metrics.alertsSent.Add(len(alerts))
+	return nil
+}
+
+// Addr of black hole notifier
+func (bh BlackHoleNotifier) Addr() string {
+	return bh.addr
+}
+
+// Close unregister the metrics
+func (bh *BlackHoleNotifier) Close() {
+	bh.metrics.alertsSent.Unregister()
+	bh.metrics.alertsSendErrors.Unregister()
+}
+
+// NewBlackHoleNotifier Create a new BlackHoleNotifier
+func NewBlackHoleNotifier() *BlackHoleNotifier {
+	address := "blackhole"
+	return &BlackHoleNotifier{
+		addr:    address,
+		metrics: newMetrics(address),
+	}
+}
diff --git a/app/vmselect/promql/binary_op.go b/app/vmselect/promql/binary_op.go
index 699f5c801..3c354946a 100644
--- a/app/vmselect/promql/binary_op.go
+++ b/app/vmselect/promql/binary_op.go
@@ -225,6 +225,14 @@ func ensureSingleTimeseries(side string, be *metricsql.BinaryOpExpr, tss []*time
 
 func groupJoin(singleTimeseriesSide string, be *metricsql.BinaryOpExpr, rvsLeft, rvsRight, tssLeft, tssRight []*timeseries) ([]*timeseries, []*timeseries, error) {
 	joinTags := be.JoinModifier.Args
+	var skipTags []string
+	if strings.ToLower(be.GroupModifier.Op) == "on" {
+		skipTags = be.GroupModifier.Args
+	}
+	joinPrefix := ""
+	if be.JoinModifierPrefix != nil {
+		joinPrefix = be.JoinModifierPrefix.S
+	}
 	type tsPair struct {
 		left  *timeseries
 		right *timeseries
@@ -234,7 +242,7 @@ func groupJoin(singleTimeseriesSide string, be *metricsql.BinaryOpExpr, rvsLeft,
 		resetMetricGroupIfRequired(be, tsLeft)
 		if len(tssRight) == 1 {
 			// Easy case - right part contains only a single matching time series.
-			tsLeft.MetricName.SetTags(joinTags, &tssRight[0].MetricName)
+			tsLeft.MetricName.SetTags(joinTags, joinPrefix, skipTags, &tssRight[0].MetricName)
 			rvsLeft = append(rvsLeft, tsLeft)
 			rvsRight = append(rvsRight, tssRight[0])
 			continue
@@ -249,7 +257,7 @@ func groupJoin(singleTimeseriesSide string, be *metricsql.BinaryOpExpr, rvsLeft,
 		for _, tsRight := range tssRight {
 			var tsCopy timeseries
 			tsCopy.CopyFromShallowTimestamps(tsLeft)
-			tsCopy.MetricName.SetTags(joinTags, &tsRight.MetricName)
+			tsCopy.MetricName.SetTags(joinTags, joinPrefix, skipTags, &tsRight.MetricName)
 			bb.B = marshalMetricTagsSorted(bb.B[:0], &tsCopy.MetricName)
 			pair, ok := m[string(bb.B)]
 			if !ok {
diff --git a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/ExploreMetricsHeader.tsx b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/ExploreMetricsHeader.tsx
index 911178b46..9d2558104 100644
--- a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/ExploreMetricsHeader.tsx
+++ b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/ExploreMetricsHeader.tsx
@@ -1,9 +1,15 @@
-import React, { FC, useMemo } from "preact/compat";
+import React, { FC, useEffect, useMemo } from "preact/compat";
 import Select from "../../Main/Select/Select";
 import "./style.scss";
 import { GRAPH_SIZES } from "../../../constants/graph";
 import classNames from "classnames";
 import useDeviceDetect from "../../../hooks/useDeviceDetect";
+import useBoolean from "../../../hooks/useBoolean";
+import { getFromStorage, saveToStorage } from "../../../utils/storage";
+import Alert from "../../Main/Alert/Alert";
+import Button from "../../Main/Button/Button";
+import { CloseIcon, TipIcon } from "../../Main/Icons";
+import Tooltip from "../../Main/Tooltip/Tooltip";
 
 interface ExploreMetricsHeaderProps {
   jobs: string[]
@@ -38,59 +44,101 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
   const noMetricsText = useMemo(() => job ? "" : "No metric names. Please select job", [job]);
   const { isMobile } = useDeviceDetect();
 
+  const {
+    value: showTips,
+    toggle: toggleShowTips,
+    setFalse: setHideTips,
+  } = useBoolean(getFromStorage("EXPLORE_METRICS_TIPS") !== "false");
+
+  useEffect(() => {
+    saveToStorage("EXPLORE_METRICS_TIPS", `${showTips}`);
+  }, [showTips]);
+
   return (
-    <div
-      className={classNames({
-        "vm-explore-metrics-header": true,
-        "vm-explore-metrics-header_mobile": isMobile,
-        "vm-block": true,
-        "vm-block_mobile": isMobile,
-      })}
-    >
-      <div className="vm-explore-metrics-header__job">
-        <Select
-          value={job}
-          list={jobs}
-          label="Job"
-          placeholder="Please select job"
-          onChange={onChangeJob}
-          autofocus={!job}
-          searchable
-        />
-      </div>
-      <div className="vm-explore-metrics-header__instance">
-        <Select
-          value={instance}
-          list={instances}
-          label="Instance"
-          placeholder="Please select instance"
-          onChange={onChangeInstance}
-          noOptionsText={noInstanceText}
-          clearable
-          searchable
-        />
+    <>
+      <div
+        className={classNames({
+          "vm-explore-metrics-header": true,
+          "vm-explore-metrics-header_mobile": isMobile,
+          "vm-block": true,
+          "vm-block_mobile": isMobile,
+        })}
+      >
+        <div className="vm-explore-metrics-header__job">
+          <Select
+            value={job}
+            list={jobs}
+            label="Job"
+            placeholder="Please select job"
+            onChange={onChangeJob}
+            autofocus={!job}
+            searchable
+          />
+        </div>
+        <div className="vm-explore-metrics-header__instance">
+          <Select
+            value={instance}
+            list={instances}
+            label="Instance"
+            placeholder="Please select instance"
+            onChange={onChangeInstance}
+            noOptionsText={noInstanceText}
+            clearable
+            searchable
+          />
+        </div>
+        <div className="vm-explore-metrics-header__size">
+          <Select
+            label="Size graphs"
+            value={size}
+            list={sizeOptions}
+            onChange={onChangeSize}
+          />
+          <Tooltip title={`${showTips ? "Hide" : "Show"} tip`}>
+            <Button
+              variant="text"
+              color={showTips ? "warning" : "gray"}
+              startIcon={<TipIcon/>}
+              onClick={toggleShowTips}
+            />
+          </Tooltip>
+        </div>
+        <div className="vm-explore-metrics-header-metrics">
+          <Select
+            label={"Metrics"}
+            value={selectedMetrics}
+            list={names}
+            placeholder="Search metric name"
+            onChange={onToggleMetric}
+            noOptionsText={noMetricsText}
+            clearable
+            searchable
+          />
+        </div>
       </div>
-      <div className="vm-explore-metrics-header__size">
-        <Select
-          label="Size graphs"
-          value={size}
-          list={sizeOptions}
-          onChange={onChangeSize}
-        />
-      </div>
-      <div className="vm-explore-metrics-header-metrics">
-        <Select
-          label={"Metrics"}
-          value={selectedMetrics}
-          list={names}
-          placeholder="Search metric name"
-          onChange={onToggleMetric}
-          noOptionsText={noMetricsText}
-          clearable
-          searchable
-        />
-      </div>
-    </div>
+
+      {showTips && (
+        <Alert variant={"warning"}>
+          <div className="vm-explore-metrics-header-description">
+            <p>
+              Please note: this page is solely designed for exploring Prometheus metrics.
+              Prometheus metrics always contain <code>job</code> and <code>instance</code> labels
+              (see <a
+                className="vm-link vm-link_colored"
+                href="https://prometheus.io/docs/concepts/jobs_instances/"
+              >these docs</a>), and this page relies on them as filters. <br/>
+              Please use this page for Prometheus metrics only, in accordance with their naming conventions.
+            </p>
+            <Button
+              variant="text"
+              size="small"
+              startIcon={<CloseIcon/>}
+              onClick={setHideTips}
+            />
+          </div>
+        </Alert>
+      )}
+    </>
   );
 };
 
diff --git a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/style.scss b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/style.scss
index 8eb2d0db3..4b598ed47 100644
--- a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/style.scss
+++ b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricsHeader/style.scss
@@ -26,6 +26,26 @@
   &__size {
     flex-grow: 1;
     min-width: 150px;
+    display: grid;
+    grid-template-columns: 1fr auto;
+    align-items: center;
+    gap: $padding-global;
+  }
+
+  &-description {
+    display: grid;
+    grid-template-columns: 1fr auto;
+    align-items: flex-start;
+    gap: $padding-small;
+
+    button {
+      color: inherit;
+      min-height: 29px;
+    }
+
+    code {
+      margin: 0 3px;
+    }
   }
 
   &-metrics {
diff --git a/app/vmui/packages/vmui/src/router/index.ts b/app/vmui/packages/vmui/src/router/index.ts
index 9bacbe696..4e6446123 100644
--- a/app/vmui/packages/vmui/src/router/index.ts
+++ b/app/vmui/packages/vmui/src/router/index.ts
@@ -42,7 +42,7 @@ export const routerOptions: {[key: string]: RouterOptions} = {
     ...routerOptionsDefault
   },
   [router.metrics]: {
-    title: "Explore metrics",
+    title: "Explore Prometheus metrics",
     header: {
       tenant: true,
       stepControl: true,
diff --git a/app/vmui/packages/vmui/src/utils/storage.ts b/app/vmui/packages/vmui/src/utils/storage.ts
index 8a28d0298..05734dfd2 100644
--- a/app/vmui/packages/vmui/src/utils/storage.ts
+++ b/app/vmui/packages/vmui/src/utils/storage.ts
@@ -9,6 +9,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
     | "TIMEZONE"
     | "THEME"
     | "LOGS_LIMIT"
+    | "EXPLORE_METRICS_TIPS"
 
 export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
   if (value) {
diff --git a/dashboards/victoriametrics-cluster.json b/dashboards/victoriametrics-cluster.json
index deab75e2a..d4bdce4ab 100644
--- a/dashboards/victoriametrics-cluster.json
+++ b/dashboards/victoriametrics-cluster.json
@@ -3503,7 +3503,7 @@
               },
               "editorMode": "code",
               "exemplar": true,
-              "expr": "max(\n    vm_cache_size_bytes{job=~\"$job_storage\", instance=~\"$instance\"} \n    /\n    vm_cache_size_max_bytes{job=~\"$job\", instance=~\"$instance\"}\n) by(type)",
+              "expr": "max(\n    vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"} \n    /\n    vm_cache_size_max_bytes{job=~\"$job\", instance=~\"$instance\"}\n) by(type)",
               "interval": "",
               "legendFormat": "__auto",
               "range": true,
diff --git a/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml b/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml
index b0adf20b4..5f3fde3ef 100644
--- a/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml
+++ b/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml
@@ -23,7 +23,7 @@ services:
 
   # Run `make package-victoria-logs` to build victoria-logs image
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:latest
+    image: docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
     volumes:
       - victorialogs-filebeat-docker-vl:/vlogs
     ports:
diff --git a/deployment/docker/victorialogs/filebeat-syslog/docker-compose.yml b/deployment/docker/victorialogs/filebeat-syslog/docker-compose.yml
index 1a5cf59e2..af3113d52 100644
--- a/deployment/docker/victorialogs/filebeat-syslog/docker-compose.yml
+++ b/deployment/docker/victorialogs/filebeat-syslog/docker-compose.yml
@@ -14,7 +14,7 @@ services:
 
   # Run `make package-victoria-logs` to build victoria-logs image
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:latest
+    image: docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
     volumes:
       - victorialogs-filebeat-syslog-vl:/vlogs
     ports:
diff --git a/deployment/docker/victorialogs/fluentbit-docker/docker-compose.yml b/deployment/docker/victorialogs/fluentbit-docker/docker-compose.yml
index 4ab0441df..f91b7cd02 100644
--- a/deployment/docker/victorialogs/fluentbit-docker/docker-compose.yml
+++ b/deployment/docker/victorialogs/fluentbit-docker/docker-compose.yml
@@ -12,7 +12,7 @@ services:
 
   # Run `make package-victoria-logs` to build victoria-logs image
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:latest
+    image: docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
     volumes:
       - victorialogs-fluentbit-vl:/vlogs
     ports:
@@ -21,4 +21,4 @@ services:
       - -storageDataPath=/vlogs
 
 volumes:
-  victorialogs-fluentbit-vl:
\ No newline at end of file
+  victorialogs-fluentbit-vl:
diff --git a/deployment/docker/victorialogs/logstash/docker-compose.yml b/deployment/docker/victorialogs/logstash/docker-compose.yml
index 5d98ea043..717216fab 100644
--- a/deployment/docker/victorialogs/logstash/docker-compose.yml
+++ b/deployment/docker/victorialogs/logstash/docker-compose.yml
@@ -15,7 +15,7 @@ services:
 
   # Run `make package-victoria-logs` to build victoria-logs image
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:latest
+    image: docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
     volumes:
       - victorialogs-logstash-vl:/vlogs
     ports:
@@ -24,4 +24,4 @@ services:
       - -storageDataPath=/vlogs
 
 volumes:
-  victorialogs-logstash-vl:
\ No newline at end of file
+  victorialogs-logstash-vl:
diff --git a/deployment/docker/victorialogs/vector-docker/docker-compose.yml b/deployment/docker/victorialogs/vector-docker/docker-compose.yml
index 278b9c33a..27b81089a 100644
--- a/deployment/docker/victorialogs/vector-docker/docker-compose.yml
+++ b/deployment/docker/victorialogs/vector-docker/docker-compose.yml
@@ -28,7 +28,7 @@ services:
 
   # Run `make package-victoria-logs` to build victoria-logs image
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:latest
+    image: docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
     volumes:
       - victorialogs-vector-docker-vl:/vlogs
     ports:
@@ -61,4 +61,4 @@ services:
 
 volumes:
   victorialogs-vector-docker-vl:
-  victorialogs-vector-docker-vm:
\ No newline at end of file
+  victorialogs-vector-docker-vm:
diff --git a/deployment/logs-benchmark/docker-compose.yml b/deployment/logs-benchmark/docker-compose.yml
index 8dd49648f..ce8f98ec7 100644
--- a/deployment/logs-benchmark/docker-compose.yml
+++ b/deployment/logs-benchmark/docker-compose.yml
@@ -44,7 +44,7 @@ services:
 
   # Run `make package-victoria-logs` to build victoria-logs image
   vlogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.1.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
     volumes:
       - vlogs:/vlogs
     ports:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 310b871e0..ac824132e 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -12,7 +12,7 @@ aliases:
 
 # CHANGELOG
 
-The following tip changes can be tested by building VictoriaMetrics components from the latest commits according to the following docs:
+The following `tip` changes can be tested by building VictoriaMetrics components from the latest commits according to the following docs:
 
 * [How to build single-node VictoriaMetrics](https://docs.victoriametrics.com/#how-to-build-from-sources)
 * [How to build cluster version of VictoriaMetrics](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#building-from-sources)
@@ -30,6 +30,9 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * FEATURE: reduce memory usage by up to 5x for setups with [high churn rate](https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate) and long [retention](https://docs.victoriametrics.com/#retention). See [description for this change](https://github.com/VictoriaMetrics/VictoriaMetrics/commit/7094fa38bc207c7bd7330ea8a834310a310ce5e3) for details.
 * FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): allow selecting time series matching at least one of multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}` selects series with either `{env="prod",job="a"}` or `{env="dev",job="b"}` labels. This functionality allows passing the selected series to [rollup functions](https://docs.victoriametrics.com/MetricsQL.html#rollup-functions) without the need to use [subqueries](https://docs.victoriametrics.com/MetricsQL.html#subqueries). See [these docs](https://docs.victoriametrics.com/keyConcepts.html#filtering-by-multiple-or-filters).
 * FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add ability to preserve metric names for binary operation results via `keep_metric_names` modifier. For example, `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in division results. See [these docs](https://docs.victoriametrics.com/MetricsQL.html#keep_metric_names). This helps to address issues like [this one](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3710).
+* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add ability to copy all the labels from `one` side of [many-to-one operations](https://prometheus.io/docs/prometheus/latest/querying/operators/#many-to-one-and-one-to-many-vector-matches) by specifying `*` inside `group_left()` or `group_right()`. Also allow adding a prefix for copied label names via `group_left(*) prefix "..."` syntax. For example, the following query copies Kubernetes namespace labels to `kube_pod_info` series and adds `ns_` prefix for the copied label names: `kube_pod_info * on(namespace) group_left(*) prefix "ns_" kube_namespace_labels`. The labels from `on()` list aren't prefixed.
+  This feature resolves [this](https://stackoverflow.com/questions/76661818/how-to-add-namespace-labels-to-pod-labels-in-prometheus)
+  and [that](https://stackoverflow.com/questions/76653997/how-can-i-make-a-new-copy-of-kube-namespace-labels-metric-with-a-different-name) questions at StackOverflow.
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add verbose output for docker installations or when TTY isn't available. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4081).
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): interrupt backoff retries when import process is cancelled. The change makes vmctl more responsive in case of errors during the import. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4442).
 * FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): update backoff policy on retries to reduce probability of overloading for `source` or `destination` databases. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4402).
@@ -60,6 +63,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
 * BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): reset evaluation timestamp after modifying group interval. Before, there could have latency on rule evaluation time.
 * BUGFIX: vmselect: fix timestamp alignment for Prometheus querying API if time argument is less than 10m from the beginning of Unix epoch.
 * BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): properly parse binary operations with reserved words on the right side such as `foo + (on{bar="baz"})`. Previously such queries could lead to panic. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4422).
+* BUGFIX: [Official Grafana dashboards for VictoriaMetrics](https://grafana.com/orgs/victoriametrics): display cache usage for all components on panel `Cache usage % by type` for cluster dashboard. Before, only vmstorage caches were shown.
 
 ## [v1.91.3](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.3)
 
diff --git a/docs/MetricsQL.md b/docs/MetricsQL.md
index a57dace4c..b574e0268 100644
--- a/docs/MetricsQL.md
+++ b/docs/MetricsQL.md
@@ -56,7 +56,7 @@ Feel free [filing a feature request](https://github.com/VictoriaMetrics/Victoria
 This functionality can be evaluated at [VictoriaMetrics playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/)
 or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/#how-to-start-victoriametrics).
 
-The list of MetricsQL features:
+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).
@@ -70,6 +70,13 @@ The list of MetricsQL features:
 * [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 either `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
   See [these docs](https://docs.victoriametrics.com/keyConcepts.html#filtering-by-multiple-or-filters) for details.
+* Support for `group_left(*)` and `group_right(*)` for copying all the labels from time series on the `one` side
+  of [many-to-one operations](https://prometheus.io/docs/prometheus/latest/querying/operators/#many-to-one-and-one-to-many-vector-matches).
+  The copied label names may clash with the existing label names, so MetricsQL provides an ability to add prefix to the copied metric names
+  via `group_left(*) prefix "..."` syntax.
+  For example, the following query copies all the `namespace`-related labels from `kube_namespace_labels` to `kube_pod_info` series,
+  while adding `ns_` prefix to the copied labels: `kube_pod_info * on(namespace) group_left(*) prefix "ns_" kube_namespace_labels`.
+  Labels from the `on()` list aren't copied.
 * [Aggregate functions](#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`.
 * [@ modifier](https://prometheus.io/docs/prometheus/latest/querying/basics/#modifier) can be put anywhere in the query.
diff --git a/docs/VictoriaLogs/CHANGELOG.md b/docs/VictoriaLogs/CHANGELOG.md
new file mode 100644
index 000000000..8d6223127
--- /dev/null
+++ b/docs/VictoriaLogs/CHANGELOG.md
@@ -0,0 +1,20 @@
+# VictoriaLogs changelog
+
+The following `tip` changes can be tested by building VictoriaLogs from the latest commit of [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/) repository
+according to [these docs](https://docs.victoriametrics.com/VictoriaLogs/QuickStart.html#building-from-source-code)
+
+## tip
+
+## [v0.2.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.2.0-victorialogs)
+
+Released at 2023-07-17
+
+* FEATURE: support short form of `_time` filters over the last X minutes/hours/days/etc. For example, `_time:5m` is a short form for `_time:(now-5m, now]`, which matches logs with [timestamps](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#time-field) for the last 5 minutes. See [these docs](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html#time-filter) for details.
+* FEATURE: add ability to specify offset for the selected time range. For example, `_time:5m offset 1h` is equivalent to `_time:(now-5m-1h, now-1h]`. See [these docs](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html#time-filter) for details.
+* FEATURE: [LogsQL](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html): replace `exact_prefix("...")` with `exact("..."*)`. This makes it consistent with [i()](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html#case-insensitive-filter) filter, which can accept phrases and prefixes, e.g. `i("phrase")` and `i("phrase"*)`. See [these docs](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html#exact-prefix-filter).
+
+## [v0.1.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.1.0-victorialogs)
+
+Released at 2023-06-21
+
+Initial release
diff --git a/docs/VictoriaLogs/LogsQL.md b/docs/VictoriaLogs/LogsQL.md
index eefa21138..e15007fbb 100644
--- a/docs/VictoriaLogs/LogsQL.md
+++ b/docs/VictoriaLogs/LogsQL.md
@@ -66,7 +66,7 @@ For example, the following query returns logs with the `error` [word](#word),
 which were ingested into VictoriaLogs during the last 5 minutes:
 
 ```logsql
-error AND _time:[now-5m,now]
+error AND _time:5m
 ```
 
 This query consists of two [filters](#filters) joined with `AND` [operator](#logical-filter):
@@ -80,7 +80,7 @@ Typical LogsQL query constists of multiple [filters](#filters) joined with `AND`
 So LogsQL allows omitting `AND` words. For example, the following query is equivalent to the query above:
 
 ```logsql
-error _time:[now-5m,now]
+error _time:5m
 ```
 
 The query returns the following [log fields](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#data-model) by default:
@@ -95,34 +95,34 @@ then just refer them in the query with `field_name:*` [filter](#any-value-filter
 For example, the following query returns `host.hostname` field additionally to `_msg`, `_stream` and `_time` fields:
 
 ```logsql
-error _time:[now-5m,now] host.hostname:*
+error _time:5m host.hostname:*
 ```
 
 Suppose the query above selects too many rows because some buggy app pushes invalid error logs to VictoriaLogs. Suppose the app adds `buggy_app` [word](#word) to every log line.
 Then the following query removes all the logs from the buggy app, allowing us paying attention to the real errors:
 
 ```logsql
-_time:[now-5m,now] error NOT buggy_app
+_time:5m error NOT buggy_app
 ```
 
 This query uses `NOT` [operator](#logical-filter) for removing log lines from the buggy app. The `NOT` operator is used frequently, so it can be substituted with `!` char.
 So the following query is equivalent to the previous one:
 
 ```logsql
-_time:[now-5m,now] error !buggy_app
+_time:5m error !buggy_app
 ```
 
 Suppose another buggy app starts pushing invalid error logs to VictoriaLogs - it adds `foobar` [word](#word) to every emitted log line.
 No problems - just add `!foobar` to the query in order to remove these buggy logs:
 
 ```logsql
-_time:[now-5m,now] error !buggy_app !foobar
+_time:5m error !buggy_app !foobar
 ```
 
 This query can be rewritten to more clear query with the `OR` [operator](#logical-filter) inside parentheses:
 
 ```logsql
-_time:[now-5m,now] error !(buggy_app OR foobar)
+_time:5m error !(buggy_app OR foobar)
 ```
 
 Note that the parentheses are required here, since otherwise the query won't return the expected results.
@@ -138,26 +138,26 @@ This word can be stored in other [field](https://docs.victoriametrics.com/Victor
 How to select error logs in this case? Just add the `log.level:` prefix in front of the `error` word:
 
 ```logsq
-_time:[now-5m,now] log.level:error !(buggy_app OR foobar)
+_time:5m log.level:error !(buggy_app OR foobar)
 ```
 
 The field name can be wrapped into quotes if it contains special chars or keywords, which may clash with LogsQL syntax.
 Any [word](#word) also can be wrapped into quotes. So the following query is equivalent to the previous one:
 
 ```logsql
-"_time":[now-5m,now] "log.level":"error" !("buggy_app" OR "foobar")
+"_time":"5m" "log.level":"error" !("buggy_app" OR "foobar")
 ```
 
 What if the application identifier - such as `buggy_app` and `foobar` - is stored in the `app` field? Correct - just add `app:` prefix in front of `buggy_app` and `foobar`:
 
 ```logsql
-_time:[now-5m,now] log.level:error !(app:buggy_app OR app:foobar)
+_time:5m log.level:error !(app:buggy_app OR app:foobar)
 ```
 
 The query can be simplified by moving the `app:` prefix outside the parentheses:
 
 ```logsql
-_time:[now-5m,now] log.level:error !app:(buggy_app OR foobar)
+_time:5m log.level:error !app:(buggy_app OR foobar)
 ```
 
 The `app` field uniquely identifies the application instance if a single instance runs per each unique `app`.
@@ -167,7 +167,7 @@ and query performance when querying the needed streams via [`_stream` filter](#s
 If the `app` field is associated with the log stream, then the query above can be rewritten to more performant one:
 
 ```logsql
-_time:[now-5m,now] log.level:error _stream:{app!~"buggy_app|foobar"}
+_time:5m log.level:error _stream:{app!~"buggy_app|foobar"}
 ```
 
 This query completely skips scanning for logs from `buggy_app` and `foobar` apps, thus significantly reducing disk read IO and CPU time
@@ -254,38 +254,42 @@ For example, the following query returns [log messages](https://docs.victoriamet
 ingested into VictoriaLogs during the last hour, which contain the `error` [word](#word):
 
 ```logsql
-_time:(now-1h, now) AND error
+_time:1h AND error
 ```
 
 The following formats are supported for `_time` filter:
 
-- Fixed time:
-  - `_time:YYYY-MM-DD` - matches all the log messages for the particular day. For example, `_time:2023-04-25` matches all the log messages for April 25, 2023 by UTC.
-  - `_time:YYYY-MM` - matches all the log messages for the particular month. For example, `_time:2023-02` matches all the log messages for February, 2023 by UTC.
-  - `_time:YYYY` - matches all the log messages for the particular year. For example, `_time:2023` matches all the log message for 2023 by UTC.
-  - `_time:YYYY-MM-DDTHH` - matches all the log messages for the particular hour. For example, `_time:2023-04-25T22` matches all the log messages from `22:00` to `23:00`
-    on April 25, 2023 by UTC.
-  - `_time:YYYY-MM-DDTHH:MM` - matches all the log messages for the particular minute. For example, `_time:2023-04-25T22:45` matches all the log messages from `22:45` to `22:46`
-    on April 25, 2023 by UTC.
-  - `_time:YYYY-MM-DDTHH:MM:SS` - matches all the log messages for the particular second. For example, `_time:2023-04-25T22:45:59` matches all the log messages
-    from `22:45:59` to `23:46:00` on April 25, 2023 by UTC.
-
-- Time range:
-  - `_time:[min_time, max_time]` - matches log messages on the time range `[min_time, max_time]`, including both `min_time` and `max_time`.
+- `_time:duration` matches logs with timestamps on the time range `(now-duration, now]`. Examples:
+  - `_time:5m` - returns logs for the last 5 minutes
+  - `_time:2.5d15m42.345s` - returns logs for the last 2.5 days, 15 minutes and 42.345 seconds
+  - `_time:1y` - returns logs for the last year
+- `_time:YYYY-MM-DD` - matches all the logs for the particular day by UTC. For example, `_time:2023-04-25` matches logs on April 25, 2023 by UTC.
+- `_time:YYYY-MM` - matches all the logs for the particular month by UTC. For example, `_time:2023-02` matches logs on February, 2023 by UTC.
+- `_time:YYYY` - matches all the logs for the particular year by UTC. For example, `_time:2023` matches logs on 2023 by UTC.
+- `_time:YYYY-MM-DDTHH` - matches all the logs for the particular hour by UTC. For example, `_time:2023-04-25T22` matches logs on April 25, 2023 at 22 hour by UTC.
+- `_time:YYYY-MM-DDTHH:MM` - matches all the logs for the particular minute by UTC. For example, `_time:2023-04-25T22:45` matches logs on April 25, 2023 at 22:45 by UTC.
+- `_time:YYYY-MM-DDTHH:MM:SS` - matches all the logs for the particular second by UTC. For example, `_time:2023-04-25T22:45:59` matches logs on April 25, 2023 at 22:45:59 by UTC.
+- `_time:[min_time, max_time]` - matches logs on the time range `[min_time, max_time]`, including both `min_time` and `max_time`.
     The `min_time` and `max_time` can contain any format specified [here](https://docs.victoriametrics.com/#timestamp-formats).
-    For example, `_time:[2023-04-01, 2023-04-30]` matches log messages for the whole April, 2023 by UTC, e.g. it is equivalent to `_time:2023-04`.
-  - `_time:[min_time, max_time)` - matches log messages on the time range `[min_time, max_time)`, not including `max_time`.
+    For example, `_time:[2023-04-01, 2023-04-30]` matches logs for the whole April, 2023 by UTC, e.g. it is equivalent to `_time:2023-04`.
+- `_time:[min_time, max_time)` - matches logs on the time range `[min_time, max_time)`, not including `max_time`.
     The `min_time` and `max_time` can contain any format specified [here](https://docs.victoriametrics.com/#timestamp-formats).
-    For example, `_time:[2023-02-01, 2023-03-01)` matches log messages for the whole February, 2023 by UTC, e.g. it is equivalent to `_time:2023-02`.
+    For example, `_time:[2023-02-01, 2023-03-01)` matches logs for the whole February, 2023 by UTC, e.g. it is equivalent to `_time:2023-02`.
 
 It is possible to specify time zone offset for all the absolute time formats by appending `+hh:mm` or `-hh:mm` suffix.
-For example, `_time:2023-04-25+05:30` matches all the log messages on April 25, 2023 by India time zone,
-while `_time:2023-02-07:00` matches all the log messages from February, 2023 by California time zone.
+For example, `_time:2023-04-25+05:30` matches all the logs on April 25, 2023 by India time zone,
+while `_time:2023-02-07:00` matches all the logs on February, 2023 by California time zone.
+
+It is possible to specify generic offset for the selected time range by appending `offset` after the `_time` filter. Examples:
+
+- `_time:5m offset 1h` matches logs on the time range `(now-1h5m, now-1h]`.
+- `_time:2023-07 offset 5h30m` matches logs on July, 2023 by UTC with offset 5h30m.
+- `_time:[2023-02-01, 2023-03-01) offset 1w` matches logs the week before the time range `[2023-02-01, 2023-03-01)` by UTC.
 
 Performance tips:
 
 - It is recommended specifying the smallest possible time range during the search, since it reduces the amounts of log entries, which need to be scanned during the query.
-  For example, `_time:[now-1h, now]` is usually faster than `_time:[now-5h, now]`.
+  For example, `_time:1h` is usually faster than `_time:5h`.
 
 - While LogsQL supports arbitrary number of `_time:...` filters at any level of [logical filters](#logical-filter),
   it is recommended specifying a single `_time` filter at the top level of the query.
@@ -589,11 +593,11 @@ See also:
 
 ### Exact prefix filter
 
-Sometimes it is needed to find log messages starting with some prefix. This can be done with the `exact_prefix(...)` filter.
+Sometimes it is needed to find log messages starting with some prefix. This can be done with the `exact("prefix"*)` filter.
 For example, the following query matches log messages, which start from `Processing request` prefix:
 
 ```logsql
-exact_prefix("Processing request")
+exact("Processing request"*)
 ```
 
 This filter matches the following [log messages](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#message-field):
@@ -603,30 +607,30 @@ This filter matches the following [log messages](https://docs.victoriametrics.co
 
 It doesn't match the following log messages:
 
-- `processing request foobar`, since the log message starts with lowercase `p`. Use `exact_prefix("processing request") OR exact_prefix("Processing request")`
+- `processing request foobar`, since the log message starts with lowercase `p`. Use `exact("processing request"*) OR exact("Processing request"*)`
   query in this case. See [these docs](#logical-filter) for details.
 - `start: Processing request`, since the log message doesn't start with `Processing request`. Use `"Processing request"` query in this case.
   See [these docs](#phrase-filter) for details.
 
-By default the `exact_prefix()` filter is applied to the [`_msg` field](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#message-field).
-Specify the [field name](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#data-model) in front of the `exact_prefix()` filter and put a colon after it
+By default the `exact()` filter is applied to the [`_msg` field](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#message-field).
+Specify the [field name](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#data-model) in front of the `exact()` filter and put a colon after it
 if it must be searched in the given field. For example, the following query returns log entries with `log.level` field, which starts with `err` prefix:
 
 ```logsql
-log.level:exact_prefix("err")
+log.level:exact("err"*)
 ```
 
 Both the field name and the phrase can contain arbitrary [utf-8](https://en.wikipedia.org/wiki/UTF-8)-encoded chars. For example:
 
 ```logsql
-log.уровень:exact_prefix("ошиб")
+log.уровень:exact("ошиб"*)
 ```
 
 The field name can be put inside quotes if it contains special chars, which may clash with the query syntax.
 For example, the following query matches `log:level` values starting with `err` prefix:
 
 ```logsql
-"log:level":exact_prefix("err")
+"log:level":exact("err"*)
 ```
 
 See also:
@@ -661,7 +665,7 @@ For example, the following query selects all the logs for the last hour for user
 during the last day:
 
 ```logsql
-_time:[now-1h,now] AND user_id:in(_time:[now-1d,now] AND path:admin | fields user_id)
+_time:1h AND user_id:in(_time:1d AND path:admin | fields user_id)
 ```
 
 See the [Roadmap](https://docs.victoriametrics.com/VictoriaLogs/Roadmap.html) for details.
@@ -809,7 +813,7 @@ Performance tips:
   Note that the `re("error|warning")` matches `errors` as well as `warnings` [words](#word), while `error OR warning` matches
   only the specified [words](#word). See also [multi-exact filter](#multi-exact-filter).
 - Prefer moving the regexp filter to the end of the [logical filter](#logical-filter), so lightweighter filters are executed first.
-- Prefer using `exact_prefix("some prefix")` instead of `re("^some prefix")`, since the [exact_prefix()](#exact-prefix-filter) works much faster than the `re()` filter.
+- Prefer using `exact("some prefix"*)` instead of `re("^some prefix")`, since the [exact()](#exact-prefix-filter) works much faster than the `re()` filter.
 - See [other performance tips](#performance-tips).
 
 See also:
diff --git a/docs/VictoriaLogs/QuickStart.md b/docs/VictoriaLogs/QuickStart.md
index 63887d5f2..eff500a01 100644
--- a/docs/VictoriaLogs/QuickStart.md
+++ b/docs/VictoriaLogs/QuickStart.md
@@ -33,8 +33,8 @@ Just download archive for the needed Operating system and architecture, unpack i
 For example, the following commands download VictoriaLogs archive for Linux/amd64, unpack and run it:
 
 ```bash
-curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v0.1.0-victorialogs/victoria-logs-linux-amd64-v0.1.0-victorialogs.tar.gz
-tar xzf victoria-logs-linux-amd64-v0.1.0-victorialogs.tar.gz
+curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v0.2.0-victorialogs/victoria-logs-linux-amd64-v0.2.0-victorialogs.tar.gz
+tar xzf victoria-logs-linux-amd64-v0.2.0-victorialogs.tar.gz
 ./victoria-logs-prod
 ```
 
@@ -58,7 +58,7 @@ Here is the command to run VictoriaLogs in a Docker container:
 
 ```bash
 docker run --rm -it -p 9428:9428 -v ./victoria-logs-data:/victoria-logs-data \
-  docker.io/victoriametrics/victoria-logs:v0.1.0-victorialogs
+  docker.io/victoriametrics/victoria-logs:v0.2.0-victorialogs
 ```
 
 See also:
diff --git a/docs/VictoriaLogs/querying/README.md b/docs/VictoriaLogs/querying/README.md
index a3bbb5950..dbbf784a7 100644
--- a/docs/VictoriaLogs/querying/README.md
+++ b/docs/VictoriaLogs/querying/README.md
@@ -168,7 +168,7 @@ received from [streams](https://docs.victoriametrics.com/VictoriaLogs/keyConcept
 during the last 5 minutes:
 
 ```bash
-curl http://localhost:9428/select/logsql/query -d 'query=_stream:{app="nginx"} AND _time:[now-5m,now] AND error' | wc -l
+curl http://localhost:9428/select/logsql/query -d 'query=_stream:{app="nginx"} AND _time:5m AND error' | wc -l
 ```
 
 See [these docs](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html#stream-filter) about `_stream` filter,
@@ -194,7 +194,7 @@ The following example calculates stats on the number of log messages received du
 grouped by `log.level` [field](https://docs.victoriametrics.com/VictoriaLogs/keyConcepts.html#data-model):
 
 ```bash
-curl http://localhost:9428/select/logsql/query -d 'query=_time:[now-5m,now] log.level:*' | jq -r '."log.level"' | sort | uniq -c 
+curl http://localhost:9428/select/logsql/query -d 'query=_time:5m log.level:*' | jq -r '."log.level"' | sort | uniq -c 
 ```
 
 The query selects all the log messages with non-empty `log.level` field via ["any value" filter](https://docs.victoriametrics.com/VictoriaLogs/LogsQL.html#any-value-filter),
diff --git a/go.mod b/go.mod
index 1643f2243..67966835d 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	// like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b
 	github.com/VictoriaMetrics/fasthttp v1.2.0
 	github.com/VictoriaMetrics/metrics v1.24.0
-	github.com/VictoriaMetrics/metricsql v0.58.2
+	github.com/VictoriaMetrics/metricsql v0.59.0
 	github.com/aws/aws-sdk-go-v2 v1.18.1
 	github.com/aws/aws-sdk-go-v2/config v1.18.27
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71
diff --git a/go.sum b/go.sum
index 86ac95cef..5ad405925 100644
--- a/go.sum
+++ b/go.sum
@@ -69,8 +69,8 @@ github.com/VictoriaMetrics/fasthttp v1.2.0 h1:nd9Wng4DlNtaI27WlYh5mGXCJOmee/2c2b
 github.com/VictoriaMetrics/fasthttp v1.2.0/go.mod h1:zv5YSmasAoSyv8sBVexfArzFDIGGTN4TfCKAtAw7IfE=
 github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw=
 github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys=
-github.com/VictoriaMetrics/metricsql v0.58.2 h1:xECDKK13ZR7LCpepVM7nuzJTnEkQI3r0qNxQIY6uv60=
-github.com/VictoriaMetrics/metricsql v0.58.2/go.mod h1:k4UaP/+CjuZslIjd+kCigNG9TQmUqh5v0TP/nMEy90I=
+github.com/VictoriaMetrics/metricsql v0.59.0 h1:0kX7hSCcFFciVEOfoJsIkmlaLJkgoP/lmAsqlJTtTgg=
+github.com/VictoriaMetrics/metricsql v0.59.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/logstorage/filters.go b/lib/logstorage/filters.go
index 55a8a9905..9a978ce44 100644
--- a/lib/logstorage/filters.go
+++ b/lib/logstorage/filters.go
@@ -481,7 +481,7 @@ func (sf *sequenceFilter) apply(bs *blockSearch, bm *filterBitmap) {
 
 // exactPrefixFilter matches the exact prefix.
 //
-// Example LogsQL: `fieldName:exact_prefix("foo bar")
+// Example LogsQL: `fieldName:exact("foo bar"*)
 type exactPrefixFilter struct {
 	fieldName string
 	prefix    string
@@ -491,7 +491,7 @@ type exactPrefixFilter struct {
 }
 
 func (ef *exactPrefixFilter) String() string {
-	return fmt.Sprintf("%sexact_prefix(%s)", quoteFieldNameIfNeeded(ef.fieldName), quoteTokenIfNeeded(ef.prefix))
+	return fmt.Sprintf("%sexact(%s*)", quoteFieldNameIfNeeded(ef.fieldName), quoteTokenIfNeeded(ef.prefix))
 }
 
 func (ef *exactPrefixFilter) getTokens() []string {
diff --git a/lib/logstorage/parser.go b/lib/logstorage/parser.go
index 9768939d2..020028926 100644
--- a/lib/logstorage/parser.go
+++ b/lib/logstorage/parser.go
@@ -320,8 +320,6 @@ func parseGenericFilter(lex *lexer, fieldName string) (filter, error) {
 		return parseNotFilter(lex, fieldName)
 	case lex.isKeyword("exact"):
 		return parseExactFilter(lex, fieldName)
-	case lex.isKeyword("exact_prefix"):
-		return parseExactPrefixFilter(lex, fieldName)
 	case lex.isKeyword("i"):
 		return parseAnyCaseFilter(lex, fieldName)
 	case lex.isKeyword("in"):
@@ -431,7 +429,7 @@ func parseFilterForPhrase(lex *lexer, phrase, fieldName string) (filter, error)
 	}
 	switch fieldName {
 	case "_time":
-		return parseTimeFilter(lex)
+		return parseTimeFilterWithOffset(lex)
 	case "_stream":
 		return parseStreamFilter(lex)
 	default:
@@ -474,6 +472,23 @@ func parseNotFilter(lex *lexer, fieldName string) (filter, error) {
 }
 
 func parseAnyCaseFilter(lex *lexer, fieldName string) (filter, error) {
+	return parseFuncArgMaybePrefix(lex, "i", fieldName, func(phrase string, isPrefixFilter bool) (filter, error) {
+		if isPrefixFilter {
+			f := &anyCasePrefixFilter{
+				fieldName: fieldName,
+				prefix:    phrase,
+			}
+			return f, nil
+		}
+		f := &anyCasePhraseFilter{
+			fieldName: fieldName,
+			phrase:    phrase,
+		}
+		return f, nil
+	})
+}
+
+func parseFuncArgMaybePrefix(lex *lexer, funcName, fieldName string, callback func(arg string, isPrefiFilter bool) (filter, error)) (filter, error) {
 	phrase := lex.token
 	lex.nextToken()
 	if !lex.isKeyword("(") {
@@ -481,33 +496,21 @@ func parseAnyCaseFilter(lex *lexer, fieldName string) (filter, error) {
 		return parseFilterForPhrase(lex, phrase, fieldName)
 	}
 	if !lex.mustNextToken() {
-		return nil, fmt.Errorf("missing arg for i()")
+		return nil, fmt.Errorf("missing arg for %s()", funcName)
 	}
 	phrase = getCompoundFuncArg(lex)
 	isPrefixFilter := false
 	if lex.isKeyword("*") && !lex.isSkippedSpace {
 		isPrefixFilter = true
 		if !lex.mustNextToken() {
-			return nil, fmt.Errorf("missing ')' after i()")
+			return nil, fmt.Errorf("missing ')' after %s()", funcName)
 		}
 	}
 	if !lex.isKeyword(")") {
-		return nil, fmt.Errorf("unexpected token %q instead of ')' in i()", lex.token)
+		return nil, fmt.Errorf("unexpected token %q instead of ')' in %s()", lex.token, funcName)
 	}
 	lex.nextToken()
-
-	if isPrefixFilter {
-		f := &anyCasePrefixFilter{
-			fieldName: fieldName,
-			prefix:    phrase,
-		}
-		return f, nil
-	}
-	f := &anyCasePhraseFilter{
-		fieldName: fieldName,
-		phrase:    phrase,
-	}
-	return f, nil
+	return callback(phrase, isPrefixFilter)
 }
 
 func parseLenRangeFilter(lex *lexer, fieldName string) (filter, error) {
@@ -624,22 +627,19 @@ func parseSequenceFilter(lex *lexer, fieldName string) (filter, error) {
 }
 
 func parseExactFilter(lex *lexer, fieldName string) (filter, error) {
-	return parseFuncArg(lex, fieldName, func(arg string) (filter, error) {
-		ef := &exactFilter{
-			fieldName: fieldName,
-			value:     arg,
+	return parseFuncArgMaybePrefix(lex, "exact", fieldName, func(phrase string, isPrefixFilter bool) (filter, error) {
+		if isPrefixFilter {
+			f := &exactPrefixFilter{
+				fieldName: fieldName,
+				prefix:    phrase,
+			}
+			return f, nil
 		}
-		return ef, nil
-	})
-}
-
-func parseExactPrefixFilter(lex *lexer, fieldName string) (filter, error) {
-	return parseFuncArg(lex, fieldName, func(arg string) (filter, error) {
-		ef := &exactPrefixFilter{
+		f := &exactFilter{
 			fieldName: fieldName,
-			prefix:    arg,
+			value:     phrase,
 		}
-		return ef, nil
+		return f, nil
 	})
 }
 
@@ -781,6 +781,48 @@ func parseFuncArgs(lex *lexer, fieldName string, callback func(args []string) (f
 	return callback(args)
 }
 
+// startsWithYear returns true if s starts from YYYY
+func startsWithYear(s string) bool {
+	if len(s) < 4 {
+		return false
+	}
+	for i := 0; i < 4; i++ {
+		c := s[i]
+		if c < '0' || c > '9' {
+			return false
+		}
+	}
+	s = s[4:]
+	if len(s) == 0 {
+		return true
+	}
+	c := s[0]
+	return c == '-' || c == '+' || c == 'Z' || c == 'z'
+}
+
+func parseTimeFilterWithOffset(lex *lexer) (*timeFilter, error) {
+	tf, err := parseTimeFilter(lex)
+	if err != nil {
+		return nil, err
+	}
+	if !lex.isKeyword("offset") {
+		return tf, nil
+	}
+	if !lex.mustNextToken() {
+		return nil, fmt.Errorf("missing offset for _time filter %s", tf)
+	}
+	s := getCompoundToken(lex)
+	d, err := promutils.ParseDuration(s)
+	if err != nil {
+		return nil, fmt.Errorf("cannot parse offset for _time filter %s: %w", tf, err)
+	}
+	offset := int64(d)
+	tf.minTimestamp -= offset
+	tf.maxTimestamp -= offset
+	tf.stringRepr += " offset " + s
+	return tf, nil
+}
+
 func parseTimeFilter(lex *lexer) (*timeFilter, error) {
 	startTimeInclude := false
 	switch {
@@ -789,17 +831,37 @@ func parseTimeFilter(lex *lexer) (*timeFilter, error) {
 	case lex.isKeyword("("):
 		startTimeInclude = false
 	default:
-		// Try parsing '_time:YYYY-MM-DD', which transforms to '_time:[YYYY-MM-DD, YYYY-MM-DD+1)'
-		startTime, stringRepr, err := parseTime(lex)
+		s := getCompoundToken(lex)
+		sLower := strings.ToLower(s)
+		if sLower == "now" || startsWithYear(s) {
+			// Parse '_time:YYYY-MM-DD', which transforms to '_time:[YYYY-MM-DD, YYYY-MM-DD+1)'
+			t, err := promutils.ParseTimeAt(s, float64(lex.currentTimestamp)/1e9)
+			if err != nil {
+				return nil, fmt.Errorf("cannot parse _time filter: %w", err)
+			}
+			startTime := int64(t * 1e9)
+			endTime := getMatchingEndTime(startTime, s)
+			tf := &timeFilter{
+				minTimestamp: startTime,
+				maxTimestamp: endTime,
+
+				stringRepr: s,
+			}
+			return tf, nil
+		}
+		// Parse _time:duration, which transforms to '_time:(now-duration, now]'
+		d, err := promutils.ParseDuration(s)
 		if err != nil {
-			return nil, fmt.Errorf("cannot parse _time filter: %w", err)
+			return nil, fmt.Errorf("cannot parse duration in _time filter: %w", err)
+		}
+		if d < 0 {
+			d = -d
 		}
-		endTime := getMatchingEndTime(startTime, stringRepr)
 		tf := &timeFilter{
-			minTimestamp: startTime,
-			maxTimestamp: endTime,
+			minTimestamp: lex.currentTimestamp - int64(d),
+			maxTimestamp: lex.currentTimestamp,
 
-			stringRepr: stringRepr,
+			stringRepr: s,
 		}
 		return tf, nil
 	}
@@ -1082,7 +1144,6 @@ var reservedKeywords = func() map[string]struct{} {
 
 		// functions
 		"exact",
-		"exact_prefix",
 		"i",
 		"in",
 		"ipv4_range",
diff --git a/lib/logstorage/storage.go b/lib/logstorage/storage.go
index 53a45d15e..4a6f9c0e1 100644
--- a/lib/logstorage/storage.go
+++ b/lib/logstorage/storage.go
@@ -415,8 +415,8 @@ func (s *Storage) MustAddRows(lr *LogRows) {
 			tsf := TimeFormatter(ts)
 			minAllowedTsf := TimeFormatter(minAllowedDay * nsecPerDay)
 			tooSmallTimestampLogger.Warnf("skipping log entry with too small timestamp=%s; it must be bigger than %s according "+
-				"to the configured -retentionPeriod. See https://docs.victoriametrics.com/VictoriaLogs/#retention ; "+
-				"log entry: %s", &tsf, &minAllowedTsf, &rf)
+				"to the configured -retentionPeriod=%dd. See https://docs.victoriametrics.com/VictoriaLogs/#retention ; "+
+				"log entry: %s", &tsf, &minAllowedTsf, durationToDays(s.retention), &rf)
 			atomic.AddUint64(&s.rowsDroppedTooSmallTimestamp, 1)
 			continue
 		}
@@ -425,8 +425,8 @@ func (s *Storage) MustAddRows(lr *LogRows) {
 			tsf := TimeFormatter(ts)
 			maxAllowedTsf := TimeFormatter(maxAllowedDay * nsecPerDay)
 			tooBigTimestampLogger.Warnf("skipping log entry with too big timestamp=%s; it must be smaller than %s according "+
-				"to the configured -futureRetention; see https://docs.victoriametrics.com/VictoriaLogs/#retention ; "+
-				"log entry: %s", &tsf, &maxAllowedTsf, &rf)
+				"to the configured -futureRetention=%dd; see https://docs.victoriametrics.com/VictoriaLogs/#retention ; "+
+				"log entry: %s", &tsf, &maxAllowedTsf, durationToDays(s.futureRetention), &rf)
 			atomic.AddUint64(&s.rowsDroppedTooBigTimestamp, 1)
 			continue
 		}
diff --git a/lib/storage/metric_name.go b/lib/storage/metric_name.go
index 2cf81052f..c7d8e3146 100644
--- a/lib/storage/metric_name.go
+++ b/lib/storage/metric_name.go
@@ -311,8 +311,20 @@ func (mn *MetricName) GetTagValue(tagKey string) []byte {
 }
 
 // SetTags sets tags from src with keys matching addTags.
-func (mn *MetricName) SetTags(addTags []string, src *MetricName) {
+//
+// It adds prefix to copied label names.
+// skipTags contains a list of tags, which must be skipped.
+func (mn *MetricName) SetTags(addTags []string, prefix string, skipTags []string, src *MetricName) {
+	if len(addTags) == 1 && addTags[0] == "*" {
+		// Special case for copying all the tags except of skipTags from src to mn.
+		mn.setAllTags(prefix, skipTags, src)
+		return
+	}
+	bb := bbPool.Get()
 	for _, tagName := range addTags {
+		if containsString(skipTags, tagName) {
+			continue
+		}
 		if tagName == string(metricGroupTagKey) {
 			mn.MetricGroup = append(mn.MetricGroup[:0], src.MetricGroup...)
 			continue
@@ -329,19 +341,47 @@ func (mn *MetricName) SetTags(addTags []string, src *MetricName) {
 			mn.RemoveTag(tagName)
 			continue
 		}
-		found := false
-		for i := range mn.Tags {
-			t := &mn.Tags[i]
-			if string(t.Key) == tagName {
-				t.Value = append(t.Value[:0], srcTag.Value...)
-				found = true
-				break
-			}
+		bb.B = append(bb.B[:0], prefix...)
+		bb.B = append(bb.B, tagName...)
+		mn.SetTagBytes(bb.B, srcTag.Value)
+	}
+	bbPool.Put(bb)
+}
+
+var bbPool bytesutil.ByteBufferPool
+
+// SetTagBytes sets tag with the given key to the given value.
+func (mn *MetricName) SetTagBytes(key, value []byte) {
+	for i := range mn.Tags {
+		t := &mn.Tags[i]
+		if string(t.Key) == string(key) {
+			t.Value = append(t.Value[:0], value...)
+			return
 		}
-		if !found {
-			mn.AddTagBytes(srcTag.Key, srcTag.Value)
+	}
+	mn.AddTagBytes(key, value)
+}
+
+func (mn *MetricName) setAllTags(prefix string, skipTags []string, src *MetricName) {
+	bb := bbPool.Get()
+	for _, tag := range src.Tags {
+		if containsString(skipTags, bytesutil.ToUnsafeString(tag.Key)) {
+			continue
 		}
+		bb.B = append(bb.B[:0], prefix...)
+		bb.B = append(bb.B, tag.Key...)
+		mn.SetTagBytes(bb.B, tag.Value)
 	}
+	bbPool.Put(bb)
+}
+
+func containsString(a []string, s string) bool {
+	for _, x := range a {
+		if x == s {
+			return true
+		}
+	}
+	return false
 }
 
 func hasTag(tags []string, key []byte) bool {
diff --git a/vendor/github.com/VictoriaMetrics/metricsql/parser.go b/vendor/github.com/VictoriaMetrics/metricsql/parser.go
index 116bb1c11..12bd7d01a 100644
--- a/vendor/github.com/VictoriaMetrics/metricsql/parser.go
+++ b/vendor/github.com/VictoriaMetrics/metricsql/parser.go
@@ -311,7 +311,7 @@ func (p *parser) parseWithArgExpr() (*withArgExpr, error) {
 	}
 	if p.lex.Token == "(" {
 		// Parse func args.
-		args, err := p.parseIdentList()
+		args, err := p.parseIdentList(false)
 		if err != nil {
 			return nil, fmt.Errorf(`withArgExpr: cannot parse args for %q: %s`, wa.Name, err)
 		}
@@ -365,16 +365,26 @@ func (p *parser) parseExpr() (Expr, error) {
 			}
 		}
 		if isBinaryOpGroupModifier(p.lex.Token) {
-			if err := p.parseModifierExpr(&be.GroupModifier); err != nil {
+			if err := p.parseModifierExpr(&be.GroupModifier, false); err != nil {
 				return nil, err
 			}
 			if isBinaryOpJoinModifier(p.lex.Token) {
 				if isBinaryOpLogicalSet(be.Op) {
 					return nil, fmt.Errorf(`modifier %q cannot be applied to %q`, p.lex.Token, be.Op)
 				}
-				if err := p.parseModifierExpr(&be.JoinModifier); err != nil {
+				if err := p.parseModifierExpr(&be.JoinModifier, true); err != nil {
 					return nil, err
 				}
+				if strings.ToLower(p.lex.Token) == "prefix" {
+					if err := p.lex.Next(); err != nil {
+						return nil, fmt.Errorf("cannot read prefix for %s: %w", be.JoinModifier.AppendString(nil), err)
+					}
+					se, err := p.parseStringExpr()
+					if err != nil {
+						return nil, fmt.Errorf("cannot parse prefix for %s: %w", be.JoinModifier.AppendString(nil), err)
+					}
+					be.JoinModifierPrefix = se
+				}
 			}
 		}
 		e2, err := p.parseSingleExpr()
@@ -608,7 +618,7 @@ funcPrefixLabel:
 		if !isAggrFuncModifier(p.lex.Token) {
 			return nil, fmt.Errorf(`AggrFuncExpr: unexpected token %q; want aggregate func modifier`, p.lex.Token)
 		}
-		if err := p.parseModifierExpr(&ae.Modifier); err != nil {
+		if err := p.parseModifierExpr(&ae.Modifier, false); err != nil {
 			return nil, err
 		}
 	}
@@ -623,7 +633,7 @@ funcArgsLabel:
 
 		// Verify whether func suffix exists.
 		if ae.Modifier.Op == "" && isAggrFuncModifier(p.lex.Token) {
-			if err := p.parseModifierExpr(&ae.Modifier); err != nil {
+			if err := p.parseModifierExpr(&ae.Modifier, false); err != nil {
 				return nil, err
 			}
 		}
@@ -665,6 +675,18 @@ func expandWithExpr(was []*withArgExpr, e Expr) (Expr, error) {
 		if err != nil {
 			return nil, err
 		}
+		var joinModifierPrefix *StringExpr
+		if t.JoinModifierPrefix != nil {
+			jmp, err := expandWithExpr(was, t.JoinModifierPrefix)
+			if err != nil {
+				return nil, err
+			}
+			se, ok := jmp.(*StringExpr)
+			if !ok {
+				return nil, fmt.Errorf("unexpected prefix for %s; want quoted string; got %s", t.JoinModifier.AppendString(nil), jmp.AppendString(nil))
+			}
+			joinModifierPrefix = se
+		}
 		if t.Op == "+" {
 			lse, lok := left.(*StringExpr)
 			rse, rok := right.(*StringExpr)
@@ -680,6 +702,7 @@ func expandWithExpr(was []*withArgExpr, e Expr) (Expr, error) {
 		be.Right = right
 		be.GroupModifier.Args = groupModifierArgs
 		be.JoinModifier.Args = joinModifierArgs
+		be.JoinModifierPrefix = joinModifierPrefix
 		pe := parensExpr{&be}
 		return &pe, nil
 	case *FuncExpr:
@@ -1084,7 +1107,7 @@ func isKeepMetricNames(token string) bool {
 	return token == "keep_metric_names"
 }
 
-func (p *parser) parseModifierExpr(me *ModifierExpr) error {
+func (p *parser) parseModifierExpr(me *ModifierExpr, allowStar bool) error {
 	if !isIdentPrefix(p.lex.Token) {
 		return fmt.Errorf(`ModifierExpr: unexpected token %q; want "ident"`, p.lex.Token)
 	}
@@ -1098,25 +1121,40 @@ func (p *parser) parseModifierExpr(me *ModifierExpr) error {
 		// join modifier may miss ident list.
 		return nil
 	}
-	args, err := p.parseIdentList()
+	args, err := p.parseIdentList(allowStar)
 	if err != nil {
-		return err
+		return fmt.Errorf("ModifierExpr: %w", err)
 	}
 	me.Args = args
 	return nil
 }
 
-func (p *parser) parseIdentList() ([]string, error) {
+func (p *parser) parseIdentList(allowStar bool) ([]string, error) {
 	if p.lex.Token != "(" {
 		return nil, fmt.Errorf(`identList: unexpected token %q; want "("`, p.lex.Token)
 	}
-	var idents []string
-	for {
+	if err := p.lex.Next(); err != nil {
+		return nil, err
+	}
+	if allowStar && p.lex.Token == "*" {
 		if err := p.lex.Next(); err != nil {
 			return nil, err
 		}
+		if p.lex.Token != ")" {
+			return nil, fmt.Errorf(`identList: unexpected token %q after "*"; want ")"`, p.lex.Token)
+		}
+		if err := p.lex.Next(); err != nil {
+			return nil, err
+		}
+		return []string{"*"}, nil
+	}
+	var idents []string
+	for {
 		if p.lex.Token == ")" {
-			goto closeParensLabel
+			if err := p.lex.Next(); err != nil {
+				return nil, err
+			}
+			return idents, nil
 		}
 		if !isIdentPrefix(p.lex.Token) {
 			return nil, fmt.Errorf(`identList: unexpected token %q; want "ident"`, p.lex.Token)
@@ -1127,19 +1165,15 @@ func (p *parser) parseIdentList() ([]string, error) {
 		}
 		switch p.lex.Token {
 		case ",":
-			continue
+			if err := p.lex.Next(); err != nil {
+				return nil, err
+			}
 		case ")":
-			goto closeParensLabel
+			continue
 		default:
 			return nil, fmt.Errorf(`identList: unexpected token %q; want ",", ")"`, p.lex.Token)
 		}
 	}
-
-closeParensLabel:
-	if err := p.lex.Next(); err != nil {
-		return nil, err
-	}
-	return idents, nil
 }
 
 func (p *parser) parseArgListExpr() ([]Expr, error) {
@@ -1628,6 +1662,11 @@ type BinaryOpExpr struct {
 	// JoinModifier contains modifier such as "group_left" or "group_right".
 	JoinModifier ModifierExpr
 
+	// JoinModifierPrefix is an optional prefix to add to labels specified inside group_left() or group_right() lists.
+	//
+	// The syntax is `group_left(foo,bar) prefix "abc"`
+	JoinModifierPrefix *StringExpr
+
 	// If KeepMetricNames is set to true, then the operation should keep metric names.
 	KeepMetricNames bool
 
@@ -1668,6 +1707,10 @@ func (be *BinaryOpExpr) appendStringNoKeepMetricNames(dst []byte) []byte {
 	if be.JoinModifier.Op != "" {
 		dst = append(dst, ' ')
 		dst = be.JoinModifier.AppendString(dst)
+		if prefix := be.JoinModifierPrefix; prefix != nil {
+			dst = append(dst, " prefix "...)
+			dst = prefix.AppendString(dst)
+		}
 	}
 	dst = append(dst, ' ')
 	if be.needRightParens() {
@@ -1739,7 +1782,11 @@ func (me *ModifierExpr) AppendString(dst []byte) []byte {
 	dst = append(dst, me.Op...)
 	dst = append(dst, '(')
 	for i, arg := range me.Args {
-		dst = appendEscapedIdent(dst, arg)
+		if arg == "*" {
+			dst = append(dst, '*')
+		} else {
+			dst = appendEscapedIdent(dst, arg)
+		}
 		if i+1 < len(me.Args) {
 			dst = append(dst, ',')
 		}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2cbb78401..58b1dd8b0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -99,7 +99,7 @@ github.com/VictoriaMetrics/fasthttp/stackless
 # github.com/VictoriaMetrics/metrics v1.24.0
 ## explicit; go 1.20
 github.com/VictoriaMetrics/metrics
-# github.com/VictoriaMetrics/metricsql v0.58.2
+# github.com/VictoriaMetrics/metricsql v0.59.0
 ## explicit; go 1.13
 github.com/VictoriaMetrics/metricsql
 github.com/VictoriaMetrics/metricsql/binaryop


Test patch:
diff --git a/app/vmalert/manager_test.go b/app/vmalert/manager_test.go
index ba1f23dde..230808e3c 100644
--- a/app/vmalert/manager_test.go
+++ b/app/vmalert/manager_test.go
@@ -340,3 +340,21 @@ func loadCfg(t *testing.T, path []string, validateAnnotations, validateExpressio
 	}
 	return cfg
 }
+
+func TestUrlValuesToStrings(t *testing.T) {
+	mapQueryParams := map[string][]string{
+		"param1": {"param1"},
+		"param2": {"anotherparam"},
+	}
+	expectedRes := []string{"param1=param1", "param2=anotherparam"}
+	res := urlValuesToStrings(mapQueryParams)
+
+	if len(res) != len(expectedRes) {
+		t.Errorf("Expected length %d, but got %d", len(expectedRes), len(res))
+	}
+	for ind, val := range expectedRes {
+		if val != res[ind] {
+			t.Errorf("Expected %v; but got %v", val, res[ind])
+		}
+	}
+}
diff --git a/app/vmalert/notifier/init_test.go b/app/vmalert/notifier/init_test.go
index c7ab2527f..09f79d4c5 100644
--- a/app/vmalert/notifier/init_test.go
+++ b/app/vmalert/notifier/init_test.go
@@ -1,8 +1,9 @@
 package notifier
 
 import (
-	"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
 	"testing"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
 )
 
 func TestInit(t *testing.T) {
@@ -35,3 +36,89 @@ func TestInit(t *testing.T) {
 		t.Fatalf("expected to get \"127.0.0.2/api/v2/alerts\"; got %q instead", nf2.Addr())
 	}
 }
+
+func TestInitBlackHole(t *testing.T) {
+	oldBlackHole := *blackHole
+	defer func() { *blackHole = oldBlackHole }()
+
+	*blackHole = true
+
+	fn, err := Init(nil, nil, "")
+	if err != nil {
+		t.Fatalf("%s", err)
+	}
+
+	nfs := fn()
+	if len(nfs) != 1 {
+		t.Fatalf("expected to get 1 notifiers; got %d", len(nfs))
+	}
+
+	targets := GetTargets()
+	if targets == nil || targets[TargetStatic] == nil {
+		t.Fatalf("expected to get static targets in response")
+	}
+	if len(targets[TargetStatic]) != 1 {
+		t.Fatalf("expected to get 1 static targets in response; but got %d", len(targets[TargetStatic]))
+	}
+	nf1 := targets[TargetStatic][0]
+	if nf1.Addr() != "blackhole" {
+		t.Fatalf("expected to get \"blackhole\"; got %q instead", nf1.Addr())
+	}
+}
+
+func TestInitBlackHoleWithNotifierUrl(t *testing.T) {
+	oldAddrs := *addrs
+	oldBlackHole := *blackHole
+	defer func() {
+		*addrs = oldAddrs
+		*blackHole = oldBlackHole
+	}()
+
+	*addrs = flagutil.ArrayString{"127.0.0.1", "127.0.0.2"}
+	*blackHole = true
+
+	_, err := Init(nil, nil, "")
+	if err == nil {
+		t.Fatalf("Expect Init to return error; instead got no error")
+	}
+
+}
+
+func TestInitBlackHoleWithNotifierConfig(t *testing.T) {
+	oldConfigPath := *configPath
+	oldBlackHole := *blackHole
+	defer func() {
+		*configPath = oldConfigPath
+		*blackHole = oldBlackHole
+	}()
+
+	*configPath = "/dummy/path"
+	*blackHole = true
+
+	fn, err := Init(nil, nil, "")
+	if err == nil {
+		t.Fatalf("Expect Init to return error; instead got no error")
+	}
+
+	if fn != nil {
+		t.Fatalf("expected no notifiers to be returned;but got %v instead", fn())
+	}
+}
+
+func TestInitWithNotifierConfigAndAddr(t *testing.T) {
+	oldConfigPath := *configPath
+	oldAddrs := *addrs
+
+	defer func() {
+		*configPath = oldConfigPath
+		*addrs = oldAddrs
+	}()
+
+	*addrs = flagutil.ArrayString{"127.0.0.1", "127.0.0.2"}
+	*configPath = "/dummy/path"
+
+	_, err := Init(nil, nil, "")
+	if err == nil {
+		t.Fatalf("Expect Init to return error; instead got no error")
+	}
+}
diff --git a/app/vmalert/notifier/notifier_blackhole_test.go b/app/vmalert/notifier/notifier_blackhole_test.go
new file mode 100644
index 000000000..0726d1fb9
--- /dev/null
+++ b/app/vmalert/notifier/notifier_blackhole_test.go
@@ -0,0 +1,53 @@
+package notifier
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	metricset "github.com/VictoriaMetrics/metrics"
+)
+
+func TestBlackHoleNotifier_Send(t *testing.T) {
+	bh := NewBlackHoleNotifier()
+
+	if err := bh.Send(context.Background(), []Alert{{
+		GroupID:     0,
+		Name:        "alert0",
+		Start:       time.Now().UTC(),
+		End:         time.Now().UTC(),
+		Annotations: map[string]string{"a": "b", "c": "d", "e": "f"},
+	}}, nil); err != nil {
+		t.Errorf("unexpected error %s", err)
+	}
+	alertCount := bh.metrics.alertsSent.Get()
+
+	if alertCount != 1 {
+		t.Errorf("expect value 1; instead got %d", alertCount)
+	}
+}
+
+func TestBlackHoleNotifier_Close(t *testing.T) {
+	bh := NewBlackHoleNotifier()
+
+	if err := bh.Send(context.Background(), []Alert{{
+		GroupID:     0,
+		Name:        "alert0",
+		Start:       time.Now().UTC(),
+		End:         time.Now().UTC(),
+		Annotations: map[string]string{"a": "b", "c": "d", "e": "f"},
+	}}, nil); err != nil {
+		t.Errorf("unexpected error %s", err)
+	}
+
+	bh.Close()
+
+	defaultMetrics := metricset.GetDefaultSet()
+	alertMetricName := "vmalert_alerts_sent_total{addr=\"blackhole\"}"
+	for _, name := range defaultMetrics.ListMetricNames() {
+		if name == alertMetricName {
+			t.Errorf("Metric name should have unregistered.But still present")
+		}
+
+	}
+}
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index a22135358..91e28cf58 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -3575,6 +3575,102 @@ func TestExecSuccess(t *testing.T) {
 		resultExpected := []netstorage.Result{r1, r2}
 		f(q, resultExpected)
 	})
+	t.Run(`vector + vector on group_left(*)`, func(t *testing.T) {
+		t.Parallel()
+		q := `sort_desc(
+			(label_set(time(), "t1", "v123", "t2", "v3"), label_set(10, "t2", "v3", "xxx", "yy"))
+			+ on (foo, t2) group_left (*)
+			(label_set(100, "t1", "v1"), label_set(time(), "t2", "v3", "noxxx", "aa"))
+		)`
+		r1 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{2000, 2400, 2800, 3200, 3600, 4000},
+			Timestamps: timestampsExpected,
+		}
+		r1.MetricName.Tags = []storage.Tag{
+			{
+				Key:   []byte("noxxx"),
+				Value: []byte("aa"),
+			},
+			{
+				Key:   []byte("t1"),
+				Value: []byte("v123"),
+			},
+			{
+				Key:   []byte("t2"),
+				Value: []byte("v3"),
+			},
+		}
+		r2 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1010, 1210, 1410, 1610, 1810, 2010},
+			Timestamps: timestampsExpected,
+		}
+		r2.MetricName.Tags = []storage.Tag{
+			{
+				Key:   []byte("noxxx"),
+				Value: []byte("aa"),
+			},
+			{
+				Key:   []byte("t2"),
+				Value: []byte("v3"),
+			},
+			{
+				Key:   []byte("xxx"),
+				Value: []byte("yy"),
+			},
+		}
+		resultExpected := []netstorage.Result{r1, r2}
+		f(q, resultExpected)
+	})
+	t.Run(`vector + vector on group_left(*) prefix`, func(t *testing.T) {
+		t.Parallel()
+		q := `sort_desc(
+			(label_set(time(), "t1", "v123", "t2", "v3"), label_set(10, "t2", "v3", "xxx", "yy"))
+			+ on (foo, t2) group_left (*) prefix "abc_"
+			(label_set(100, "t1", "v1"), label_set(time(), "t2", "v3", "noxxx", "aa"))
+		)`
+		r1 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{2000, 2400, 2800, 3200, 3600, 4000},
+			Timestamps: timestampsExpected,
+		}
+		r1.MetricName.Tags = []storage.Tag{
+			{
+				Key:   []byte("abc_noxxx"),
+				Value: []byte("aa"),
+			},
+			{
+				Key:   []byte("t1"),
+				Value: []byte("v123"),
+			},
+			{
+				Key:   []byte("t2"),
+				Value: []byte("v3"),
+			},
+		}
+		r2 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1010, 1210, 1410, 1610, 1810, 2010},
+			Timestamps: timestampsExpected,
+		}
+		r2.MetricName.Tags = []storage.Tag{
+			{
+				Key:   []byte("abc_noxxx"),
+				Value: []byte("aa"),
+			},
+			{
+				Key:   []byte("t2"),
+				Value: []byte("v3"),
+			},
+			{
+				Key:   []byte("xxx"),
+				Value: []byte("yy"),
+			},
+		}
+		resultExpected := []netstorage.Result{r1, r2}
+		f(q, resultExpected)
+	})
 	t.Run(`vector + vector on group_left (__name__)`, func(t *testing.T) {
 		t.Parallel()
 		q := `sort_desc(
diff --git a/lib/logstorage/parser_test.go b/lib/logstorage/parser_test.go
index 90769479e..22360f6fe 100644
--- a/lib/logstorage/parser_test.go
+++ b/lib/logstorage/parser_test.go
@@ -79,6 +79,34 @@ func TestNewStreamFilterFailure(t *testing.T) {
 	f("{foo='bar' baz='x'}")
 }
 
+func TestParseTimeDuration(t *testing.T) {
+	f := func(s string, durationExpected time.Duration) {
+		t.Helper()
+		q, err := ParseQuery("_time:" + s)
+		if err != nil {
+			t.Fatalf("unexpected error: %s", err)
+		}
+		tf, ok := q.f.(*timeFilter)
+		if !ok {
+			t.Fatalf("unexpected filter; got %T; want *timeFilter; filter: %s", q.f, q.f)
+		}
+		if tf.stringRepr != s {
+			t.Fatalf("unexpected string represenation for timeFilter; got %q; want %q", tf.stringRepr, s)
+		}
+		duration := time.Duration(tf.maxTimestamp - tf.minTimestamp)
+		if duration != durationExpected {
+			t.Fatalf("unexpected duration; got %s; want %s", duration, durationExpected)
+		}
+	}
+	f("5m", 5*time.Minute)
+	f("5m offset 1h", 5*time.Minute)
+	f("5m offset -3.5h5m45s", 5*time.Minute)
+	f("-5.5m", 5*time.Minute+30*time.Second)
+	f("-5.5m offset 1d5m", 5*time.Minute+30*time.Second)
+	f("3d2h12m34s45ms", 3*24*time.Hour+2*time.Hour+12*time.Minute+34*time.Second+45*time.Millisecond)
+	f("3d2h12m34s45ms offset 10ms", 3*24*time.Hour+2*time.Hour+12*time.Minute+34*time.Second+45*time.Millisecond)
+}
+
 func TestParseTimeRange(t *testing.T) {
 	f := func(s string, minTimestampExpected, maxTimestampExpected int64) {
 		t.Helper()
@@ -234,10 +262,16 @@ func TestParseTimeRange(t *testing.T) {
 	maxTimestamp = time.Date(2023, time.April, 7, 0, 0, 0, 0, time.UTC).UnixNano() - 1
 	f(`(2023-03-01T21:20,2023-04-06]`, minTimestamp, maxTimestamp)
 
-	// _time:[start, end]
+	// _time:[start, end] with timezone
 	minTimestamp = time.Date(2023, time.February, 28, 21, 40, 0, 0, time.UTC).UnixNano()
 	maxTimestamp = time.Date(2023, time.April, 7, 0, 0, 0, 0, time.UTC).UnixNano() - 1
 	f(`[2023-03-01+02:20,2023-04-06T23]`, minTimestamp, maxTimestamp)
+
+	// _time:[start, end] with timezone and offset
+	offset := int64(30*time.Minute + 5*time.Second)
+	minTimestamp = time.Date(2023, time.February, 28, 21, 40, 0, 0, time.UTC).UnixNano() - offset
+	maxTimestamp = time.Date(2023, time.April, 7, 0, 0, 0, 0, time.UTC).UnixNano() - 1 - offset
+	f(`[2023-03-01+02:20,2023-04-06T23] offset 30m5s`, minTimestamp, maxTimestamp)
 }
 
 func TestParseSequenceFilter(t *testing.T) {
@@ -606,6 +640,7 @@ func TestParseQuerySuccess(t *testing.T) {
 	f(`_time:2023-01-02T04:05:06.789-02:30`, `_time:2023-01-02T04:05:06.789-02:30`)
 	f(`_time:2023-01-02T04:05:06.789+02:30`, `_time:2023-01-02T04:05:06.789+02:30`)
 	f(`_time:[1234567890, 1400000000]`, `_time:[1234567890,1400000000]`)
+	f(`_time:2d3h5.5m3s45ms`, `_time:2d3h5.5m3s45ms`)
 
 	// reserved keywords
 	f("and", `"and"`)
@@ -627,12 +662,6 @@ func TestParseQuerySuccess(t *testing.T) {
 	f("a:exact", `a:"exact"`)
 	f("a:exact-foo", `a:exact-foo`)
 	f("exact-foo:b", `exact-foo:b`)
-	f("exact_prefix", `"exact_prefix"`)
-	f("exact_prefix:a", `"exact_prefix":a`)
-	f("exact_prefix-foo", `exact_prefix-foo`)
-	f("a:exact_prefix", `a:"exact_prefix"`)
-	f("a:exact_prefix-foo", `a:exact_prefix-foo`)
-	f("exact_prefix-foo:b", `exact_prefix-foo:b`)
 	f("i", `"i"`)
 	f("i-foo", `i-foo`)
 	f("a:i-foo", `a:i-foo`)
@@ -676,17 +705,11 @@ func TestParseQuerySuccess(t *testing.T) {
 
 	// exact filter
 	f("exact(foo)", `exact(foo)`)
+	f("exact(foo*)", `exact(foo*)`)
 	f("exact('foo bar),|baz')", `exact("foo bar),|baz")`)
-	f(`exact(foo-bar,)`, `exact(foo-bar)`)
+	f("exact('foo bar),|baz'*)", `exact("foo bar),|baz"*)`)
 	f(`exact(foo|b:ar)`, `exact("foo|b:ar")`)
-	f(`foo:exact(f,)`, `foo:exact(f)`)
-
-	// exact_prefix filter
-	f("exact_prefix(foo)", `exact_prefix(foo)`)
-	f(`exact_prefix("foo bar")`, `exact_prefix("foo bar")`)
-	f(`exact_prefix(foo-bar,)`, `exact_prefix(foo-bar)`)
-	f(`exact_prefix(foo|b:ar)`, `exact_prefix("foo|b:ar")`)
-	f(`foo:exact_prefix(f,)`, `foo:exact_prefix(f)`)
+	f(`foo:exact(foo|b:ar*)`, `foo:exact("foo|b:ar"*)`)
 
 	// i filter
 	f("i(foo)", `i(foo)`)
@@ -849,6 +872,9 @@ func TestParseQueryFailure(t *testing.T) {
 	f("_time:[2023-01-02T04:05:06+12,2023]")
 	f("_time:[2023-01-02T04:05:06-12,2023]")
 	f("_time:2023-01-02T04:05:06.789")
+	f("_time:234foo")
+	f("_time:5m offset")
+	f("_time:10m offset foobar")
 
 	// long query with error
 	f(`very long query with error aaa ffdfd fdfdfd fdfd:( ffdfdfdfdfd`)
@@ -877,9 +903,9 @@ func TestParseQueryFailure(t *testing.T) {
 	f(`exact(f, b)`)
 	f(`exact(foo`)
 	f(`exact(foo,`)
-	f(`exact(foo*)`)
 	f(`exact(foo bar)`)
 	f(`exact(foo, bar`)
+	f(`exact(foo,)`)
 
 	// invalid i
 	f(`i(`)
diff --git a/lib/storage/storage_test.go b/lib/storage/storage_test.go
index a20b37795..24ca50c70 100644
--- a/lib/storage/storage_test.go
+++ b/lib/storage/storage_test.go
@@ -1197,12 +1197,3 @@ func TestStorageDeleteStaleSnapshots(t *testing.T) {
 		t.Fatalf("cannot remove %q: %s", path, err)
 	}
 }
-
-func containsString(a []string, s string) bool {
-	for i := range a {
-		if a[i] == s {
-			return true
-		}
-	}
-	return false
-}