Repository: VictoriaMetrics/VictoriaMetrics
Pull request #6578 (2024-07-04 13:59:17)

Automatic update Grafana datasource docs from VictoriaMetrics/victorialogs-datasource@4ed20bd


Patch:
diff --git a/app/vmalert/Makefile b/app/vmalert/Makefile
index 610a8e137..05a7399c8 100644
--- a/app/vmalert/Makefile
+++ b/app/vmalert/Makefile
@@ -101,8 +101,7 @@ replay-vmalert: vmalert
 		-remoteWrite.url=http://localhost:8428 \
 		-external.label=cluster=east-1 \
 		-external.label=replica=a \
-		-replay.timeFrom=2021-05-11T07:21:43Z \
-		-replay.timeTo=2021-05-29T18:40:43Z
+		-replay.timeFrom=2024-06-01T00:00:00Z
 
 vmalert-linux-amd64:
 	APP_NAME=vmalert CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
diff --git a/app/vmalert/main.go b/app/vmalert/main.go
index f6e05a00f..d6f164215 100644
--- a/app/vmalert/main.go
+++ b/app/vmalert/main.go
@@ -72,7 +72,7 @@ absolute path to all .tpl files in root.
 	externalAlertSource = flag.String("external.alert.source", "", `External Alert Source allows to override the Source link for alerts sent to AlertManager `+
 		`for cases where you want to build a custom link to Grafana, Prometheus or any other service. `+
 		`Supports templating - see https://docs.victoriametrics.com/vmalert/#templating . `+
-		`For example, link to Grafana: -external.alert.source='explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{$expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"now-1h","to":"now"}}'. `+
+		`For example, link to Grafana: -external.alert.source='explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{.Expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"now-1h","to":"now"}}'. `+
 		`Link to VMUI: -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'. `+
 		`If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.`)
 	externalLabels = flagutil.NewArrayString("external.label", "Optional label in the form 'Name=value' to add to all generated recording rules and alerts. "+
@@ -132,7 +132,7 @@ func main() {
 		validateTplFn = notifier.ValidateTemplates
 	}
 
-	if *replayFrom != "" || *replayTo != "" {
+	if *replayFrom != "" {
 		rw, err := remotewrite.Init(context.Background())
 		if err != nil {
 			logger.Fatalf("failed to init remoteWrite: %s", err)
diff --git a/app/vmalert/replay.go b/app/vmalert/replay.go
index 9559e85fe..162da1ba6 100644
--- a/app/vmalert/replay.go
+++ b/app/vmalert/replay.go
@@ -15,9 +15,10 @@ import (
 
 var (
 	replayFrom = flag.String("replay.timeFrom", "",
-		"The time filter in RFC3339 format to select time series with timestamp equal or higher than provided value. E.g. '2020-01-01T20:07:00Z'")
+		"The time filter in RFC3339 format to start the replay from. E.g. '2020-01-01T20:07:00Z'")
 	replayTo = flag.String("replay.timeTo", "",
-		"The time filter in RFC3339 format to select timeseries with timestamp equal or lower than provided value. E.g. '2020-01-01T20:07:00Z'")
+		"The time filter in RFC3339 format to finish the replay by. E.g. '2020-01-01T20:07:00Z'. "+
+			"By default, is set to the current time.")
 	replayRulesDelay = flag.Duration("replay.rulesDelay", time.Second,
 		"Delay between rules evaluation within the group. Could be important if there are chained rules inside the group "+
 			"and processing need to wait for previous rule results to be persisted by remote storage before evaluating the next rule."+
@@ -36,14 +37,20 @@ func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewri
 	}
 	tFrom, err := time.Parse(time.RFC3339, *replayFrom)
 	if err != nil {
-		return fmt.Errorf("failed to parse %q: %w", *replayFrom, err)
+		return fmt.Errorf("failed to parse replay.timeFrom=%q: %w", *replayFrom, err)
 	}
-	tTo, err := time.Parse(time.RFC3339, *replayTo)
-	if err != nil {
-		return fmt.Errorf("failed to parse %q: %w", *replayTo, err)
+
+	// use tFrom location for default value, otherwise filters could have different locations
+	tTo := time.Now().In(tFrom.Location())
+	if *replayTo != "" {
+		tTo, err = time.Parse(time.RFC3339, *replayTo)
+		if err != nil {
+			return fmt.Errorf("failed to parse replay.timeTo=%q: %w", *replayTo, err)
+		}
 	}
+
 	if !tTo.After(tFrom) {
-		return fmt.Errorf("replay.timeTo must be bigger than replay.timeFrom")
+		return fmt.Errorf("replay.timeTo=%v must be bigger than replay.timeFrom=%v", tTo, tFrom)
 	}
 	labels := make(map[string]string)
 	for _, s := range *externalLabels {
diff --git a/deployment/docker/docker-compose-cluster.yml b/deployment/docker/docker-compose-cluster.yml
index 3f55a272c..6821850b6 100644
--- a/deployment/docker/docker-compose-cluster.yml
+++ b/deployment/docker/docker-compose-cluster.yml
@@ -145,8 +145,7 @@ services:
       - '--rule=/etc/alerts/*.yml'
       # display source of alerts in grafana
       - '-external.url=http://127.0.0.1:3000' #grafana outside container
-      # when copypaste the line below be aware of '$$' for escaping in '$expr'
-      - '--external.alert.source=explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{$$expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"now-1h","to":"now"}}'
+      - '--external.alert.source=explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{.Expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"{{ .ActiveAt.UnixMilli }}","to":"now"}}'
     restart: always
 
   # alertmanager receives alerting notifications from vmalert
diff --git a/deployment/docker/docker-compose-victorialogs.yml b/deployment/docker/docker-compose-victorialogs.yml
index c840efeaf..23b684c8d 100644
--- a/deployment/docker/docker-compose-victorialogs.yml
+++ b/deployment/docker/docker-compose-victorialogs.yml
@@ -40,7 +40,7 @@ services:
 #  storing logs and serving read queries.
   victorialogs:
     container_name: victorialogs
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
     command:
       - "--storageDataPath=/vlogs"
       - "--httpListenAddr=:9428"
diff --git a/deployment/docker/docker-compose.yml b/deployment/docker/docker-compose.yml
index 8be7e6501..812a0f7be 100644
--- a/deployment/docker/docker-compose.yml
+++ b/deployment/docker/docker-compose.yml
@@ -84,8 +84,7 @@ services:
       - "--rule=/etc/alerts/*.yml"
       # display source of alerts in grafana
       - "--external.url=http://127.0.0.1:3000" #grafana outside container
-      # when copypaste the line be aware of '$$' for escaping in '$expr'
-      - '--external.alert.source=explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{$$expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"now-1h","to":"now"}}'
+      - '--external.alert.source=explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{.Expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"{{ .ActiveAt.UnixMilli }}","to":"now"}}'
     networks:
       - vm_net
     restart: always
diff --git a/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml b/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml
index b599169fc..c1234109c 100644
--- a/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml
+++ b/deployment/docker/victorialogs/filebeat-docker/docker-compose.yml
@@ -22,7 +22,7 @@ services:
       - -beat.uri=http://filebeat-victorialogs:5066
 
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-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 b7b878d1d..f6724cef3 100644
--- a/deployment/docker/victorialogs/filebeat-syslog/docker-compose.yml
+++ b/deployment/docker/victorialogs/filebeat-syslog/docker-compose.yml
@@ -13,7 +13,7 @@ services:
       - "5140:5140"
 
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-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 7188e51e8..29d227431 100644
--- a/deployment/docker/victorialogs/fluentbit-docker/docker-compose.yml
+++ b/deployment/docker/victorialogs/fluentbit-docker/docker-compose.yml
@@ -11,7 +11,7 @@ services:
       - "5140:5140"
 
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
     volumes:
       - victorialogs-fluentbit-vl:/vlogs
     ports:
diff --git a/deployment/docker/victorialogs/logstash/docker-compose.yml b/deployment/docker/victorialogs/logstash/docker-compose.yml
index 556538365..1e5fe64ae 100644
--- a/deployment/docker/victorialogs/logstash/docker-compose.yml
+++ b/deployment/docker/victorialogs/logstash/docker-compose.yml
@@ -14,7 +14,7 @@ services:
       - "5140:5140"
 
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
     volumes:
       - victorialogs-logstash-vl:/vlogs
     ports:
diff --git a/deployment/docker/victorialogs/promtail/docker-compose.yml b/deployment/docker/victorialogs/promtail/docker-compose.yml
index 81423820d..ea7129c5c 100644
--- a/deployment/docker/victorialogs/promtail/docker-compose.yml
+++ b/deployment/docker/victorialogs/promtail/docker-compose.yml
@@ -12,7 +12,7 @@ services:
       - "5140:5140"
 
   vlogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
     volumes:
       - victorialogs-promtail-docker:/vlogs
     ports:
diff --git a/deployment/docker/victorialogs/vector-docker/docker-compose.yml b/deployment/docker/victorialogs/vector-docker/docker-compose.yml
index b1760e2c2..27c1c239c 100644
--- a/deployment/docker/victorialogs/vector-docker/docker-compose.yml
+++ b/deployment/docker/victorialogs/vector-docker/docker-compose.yml
@@ -22,7 +22,7 @@ services:
         condition: service_healthy
 
   victorialogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
     volumes:
       - victorialogs-vector-docker-vl:/vlogs
     ports:
diff --git a/deployment/logs-benchmark/docker-compose.yml b/deployment/logs-benchmark/docker-compose.yml
index 3602bf723..79ecb2325 100644
--- a/deployment/logs-benchmark/docker-compose.yml
+++ b/deployment/logs-benchmark/docker-compose.yml
@@ -3,7 +3,7 @@ version: '3'
 services:
   # Run `make package-victoria-logs` to build victoria-logs image
   vlogs:
-    image: docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+    image: docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
     volumes:
       - vlogs:/vlogs
     ports:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 93bc51cc7..ebac923e8 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -47,6 +47,7 @@ See also [LTS releases](https://docs.victoriametrics.com/lts-releases/).
   * These and other metrics were reflected on the [vmagent dashboard](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/dashboards/vmagent.json) in `stream aggregation` section.
 * FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent/) and [Single-node VictoriaMetrics](https://docs.victoriametrics.com/): add `-graphite.sanitizeMetricName` cmd-line flag for sanitizing metrics ingested via [Graphite protocol](https://docs.victoriametrics.com/#how-to-send-data-from-graphite-compatible-agents-such-as-statsd). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6077).
 * FEATURE: [VictoriaMetrics cluster](https://docs.victoriametrics.com/cluster-victoriametrics/): do not retry RPC calls to vmstorage nodes if [complexity limits](https://docs.victoriametrics.com/#resource-usage-limits) were exceeded.
+* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert/): make `-replay.timeTo` optional in [replay mode](https://docs.victoriametrics.com/vmalert/#rules-backfilling). When omitted, the current timestamp will be used. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6492).
 
 * BUGFIX: [docker-compose](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#docker-compose-environment-for-victoriametrics): fix incorrect link to vmui from [VictoriaMetrics plugin in Grafana](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#grafana).
 * BUGFIX: [docker-compose](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#docker-compose-environment-for-victoriametrics): fix incorrect link to vmui from [VictoriaMetrics plugin in Grafana](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker#grafana).
diff --git a/docs/VictoriaLogs/CHANGELOG.md b/docs/VictoriaLogs/CHANGELOG.md
index 67fd095d6..d5f7c3132 100644
--- a/docs/VictoriaLogs/CHANGELOG.md
+++ b/docs/VictoriaLogs/CHANGELOG.md
@@ -19,6 +19,12 @@ according to [these docs](https://docs.victoriametrics.com/victorialogs/quicksta
 
 ## tip
 
+## [v0.27.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.27.1-victorialogs)
+
+Released at 2024-07-05
+
+* BUGFIX: properly JSON-encode strings with special chars in [HTTP querying API](https://docs.victoriametrics.com/victorialogs/querying/#http-api) responses. This fixes the `error decode response: invalid character 'x' in string escape code` error in [VictoriaLogs datasource for Grafana](https://github.com/VictoriaMetrics/victorialogs-datasource/). See [this issue](https://github.com/VictoriaMetrics/victorialogs-datasource/issues/24). The issue has been introduced in the release [v0.9.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.9.0-victorialogs).
+
 ## [v0.27.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v0.27.0-victorialogs)
 
 Released at 2024-07-02
diff --git a/docs/VictoriaLogs/QuickStart.md b/docs/VictoriaLogs/QuickStart.md
index 326721f07..31ffbdd30 100644
--- a/docs/VictoriaLogs/QuickStart.md
+++ b/docs/VictoriaLogs/QuickStart.md
@@ -36,8 +36,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:
 
 ```sh
-curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v0.27.0-victorialogs/victoria-logs-linux-amd64-v0.27.0-victorialogs.tar.gz
-tar xzf victoria-logs-linux-amd64-v0.27.0-victorialogs.tar.gz
+curl -L -O https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v0.27.1-victorialogs/victoria-logs-linux-amd64-v0.27.1-victorialogs.tar.gz
+tar xzf victoria-logs-linux-amd64-v0.27.1-victorialogs.tar.gz
 ./victoria-logs-prod
 ```
 
@@ -61,7 +61,7 @@ Here is the command to run VictoriaLogs in a Docker container:
 
 ```sh
 docker run --rm -it -p 9428:9428 -v ./victoria-logs-data:/victoria-logs-data \
-  docker.io/victoriametrics/victoria-logs:v0.27.0-victorialogs
+  docker.io/victoriametrics/victoria-logs:v0.27.1-victorialogs
 ```
 
 See also:
diff --git a/docs/VictoriaLogs/victorialogs-datasource.md b/docs/VictoriaLogs/victorialogs-datasource.md
index 3a3f30530..beccd1627 100644
--- a/docs/VictoriaLogs/victorialogs-datasource.md
+++ b/docs/VictoriaLogs/victorialogs-datasource.md
@@ -1,6 +1,7 @@
 ---
 weight: 9
 title: VictoriaLogs datasource for Grafana
+source: https://github.com/VictoriaMetrics/victorialogs-datasource/blob/main/README.md
 menu:
   docs:
     parent: victorialogs
@@ -93,7 +94,7 @@ docker-compose -f docker-compose.yaml up
 
 After Grafana starts successfully, datasource should be available in the datasources tab
 
-<img src="provision_datasources.png" width="800" alt="Configuration">
+<img src="/victorialogsprovision_datasources.png" width="800" alt="Configuration">
 
 ### Install in Kubernetes
 
diff --git a/docs/vmalert.md b/docs/vmalert.md
index 19fbe9e5d..a15f73bf1 100644
--- a/docs/vmalert.md
+++ b/docs/vmalert.md
@@ -418,6 +418,38 @@ in configured `-remoteRead.url`, weren't updated in the last `1h` (controlled by
 or received state doesn't match current `vmalert` rules configuration. `vmalert` marks successfully restored rules
 with `restored` label in [web UI](#web).
 
+### Link to alert source
+
+Alerting notifications sent by vmalert always contain a `source` link. By default, the link format
+is the following `http://<vmalert-addr>/vmalert/alert?group_id=<group_id>&alert_id=<alert_id>`. On click, it opens
+vmalert [web UI](https://docs.victoriametrics.com/vmalert/#web) to show the alert status and its fields.
+
+It is possible to override the link format. For example, to make the link to [vmui](https://docs.victoriametrics.com/single-server-victoriametrics/#vmui)
+specify the following cmd-line flags:
+```
+./bin/vmalert \
+    -external.url=http://<vmui-addr> \  # the hostname and port for datasource vmui 
+    -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}' # the path built using alert expr
+```
+
+Now, all `source` links will lead to `http://<vmui-addr>/vmui/#/?g0.expr=$expr`, where $expr is an alerting rule
+expression.
+
+The `-external.alert.source` cmd-line flag supports [templating](https://docs.victoriametrics.com/vmalert/#templating)
+and allows using labels and extra data related to the alert. For example, see the following link to Grafana:
+```
+./bin/vmalert \
+    -external.url=http://<grafana-addr> \  # the hostname and port for Grafana 
+    -external.alert.source='explore?left={"datasource":"VictoriaMetrics","queries":[{"expr":{{ .Expr|jsonEscape|queryEscape }},"refId":"A"}],"range":{"from":"{{ .ActiveAt.UnixMilli }}","to":"now"}}'
+```
+
+In this example, `-external.alert.source` will lead to Grafana's Explore page with `expr` field equal to alert expression,
+and time range will be selected starting from `"from":"{{ .ActiveAt.UnixMilli }}"` when alert became active.
+
+In addition to `source` link, some extra links could be added to alert's [annotations](https://docs.victoriametrics.com/vmalert/#alerting-rules)
+field. See [how we use them](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/839596c00df123c639d1244b28ee8137dfc9609c/deployment/docker/alerts-cluster.yml#L43) 
+to link alerting rule and the corresponding panel on Grafana dashboard.
+
 ### Multitenancy
 
 There are the following approaches exist for alerting and recording rules across
@@ -705,8 +737,8 @@ To run vmalert in `replay` mode:
 ./bin/vmalert -rule=path/to/your.rules \        # path to files with rules you usually use with vmalert
     -datasource.url=http://localhost:8428 \     # Prometheus HTTP API compatible datasource
     -remoteWrite.url=http://localhost:8428 \    # remote write compatible storage to persist results
-    -replay.timeFrom=2021-05-11T07:21:43Z \     # time from begin replay
-    -replay.timeTo=2021-05-29T18:40:43Z         # time to finish replay
+    -replay.timeFrom=2021-05-11T07:21:43Z \     # to start replay from
+    -replay.timeTo=2021-05-29T18:40:43Z         # to finish replay by, is optional
 ```
 
 The output of the command will look like the following:
@@ -738,12 +770,12 @@ max range per request:  8h20m0s
 ```
 
 In `replay` mode all groups are executed sequentially one-by-one. Rules within the group are
-executed sequentially as well (`concurrency` setting is ignored). Vmalert sends rule's expression
+executed sequentially as well (`concurrency` setting is ignored). vmalert sends rule's expression
 to [/query_range](https://docs.victoriametrics.com/keyconcepts/#range-query) endpoint
 of the configured `-datasource.url`. Returned data is then processed according to the rule type and
 backfilled to `-remoteWrite.url` via [remote Write protocol](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations).
-Vmalert respects `evaluationInterval` value set by flag or per-group during the replay.
-Vmalert automatically disables caching on VictoriaMetrics side by sending `nocache=1` param. It allows
+vmalert respects `evaluationInterval` value set by flag or per-group during the replay.
+vmalert automatically disables caching on VictoriaMetrics side by sending `nocache=1` param. It allows
 to prevent cache pollution and unwanted time range boundaries adjustment during backfilling.
 
 #### Recording rules
@@ -1064,7 +1096,7 @@ The shortlist of configuration flags is the following:
   -evaluationInterval duration
      How often to evaluate the rules (default 1m0s)
   -external.alert.source string
-     External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert/#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{$expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"now-1h","to":"now"}}'. Link to VMUI: -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'. If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
+     External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service. Supports templating - see https://docs.victoriametrics.com/vmalert/#templating . For example, link to Grafana: -external.alert.source='explore?orgId=1&left={"datasource":"VictoriaMetrics","queries":[{"expr":{{.Expr|jsonEscape|queryEscape}},"refId":"A"}],"range":{"from":"now-1h","to":"now"}}'. Link to VMUI: -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'. If empty 'vmalert/alert?group_id={{.GroupID}}&alert_id={{.AlertID}}' is used.
   -external.label array
      Optional label in the form 'Name=value' to add to all generated recording rules and alerts. Pass multiple -label flags in order to add multiple label sets.
      Supports an array of values separated by comma or specified via multiple flags.
@@ -1376,9 +1408,9 @@ The shortlist of configuration flags is the following:
   -replay.rulesDelay duration
      Delay between rules evaluation within the group. Could be important if there are chained rules inside the group and processing need to wait for previous rule results to be persisted by remote storage before evaluating the next rule.Keep it equal or bigger than -remoteWrite.flushInterval. (default 1s)
   -replay.timeFrom string
-     The time filter in RFC3339 format to select time series with timestamp equal or higher than provided value. E.g. '2020-01-01T20:07:00Z'
+     The time filter in RFC3339 format to start the replay from. E.g. '2020-01-01T20:07:00Z'
   -replay.timeTo string
-     The time filter in RFC3339 format to select timeseries with timestamp equal or lower than provided value. E.g. '2020-01-01T20:07:00Z'
+     The time filter in RFC3339 format to finish the replay by. E.g. '2020-01-01T20:07:00Z'. By default, is set to the current time.
   -rule array
      Path to the files or http url with alerting and/or recording rules.
      Supports hierarchical patterns and regexpes.
diff --git a/go.mod b/go.mod
index e5c45f1bd..f7e130f6b 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,7 @@ require (
 	github.com/valyala/fasttemplate v1.2.2
 	github.com/valyala/gozstd v1.21.1
 	github.com/valyala/histogram v1.2.0
-	github.com/valyala/quicktemplate v1.7.0
+	github.com/valyala/quicktemplate v1.8.0
 	golang.org/x/oauth2 v0.21.0
 	golang.org/x/sys v0.21.0
 	google.golang.org/api v0.187.0
diff --git a/go.sum b/go.sum
index 9e9db5e1e..417ded19b 100644
--- a/go.sum
+++ b/go.sum
@@ -86,8 +86,6 @@ github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrI
 github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
-github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
 github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
 github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
@@ -254,7 +252,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -362,8 +359,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
-github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
@@ -492,7 +487,6 @@ github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
 github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
 github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
 github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
 github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
@@ -503,9 +497,8 @@ github.com/valyala/gozstd v1.21.1 h1:TQFZVTk5zo7iJcX3o4XYBJujPdO31LFb4fVImwK873A
 github.com/valyala/gozstd v1.21.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
 github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
 github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
-github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM=
-github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8=
-github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdqCysW5k=
+github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM=
 github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
 github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
@@ -549,7 +542,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -616,8 +608,6 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
@@ -676,7 +666,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/lib/logstorage/pipe_format.go b/lib/logstorage/pipe_format.go
index a47328063..b6e80573e 100644
--- a/lib/logstorage/pipe_format.go
+++ b/lib/logstorage/pipe_format.go
@@ -3,9 +3,10 @@ package logstorage
 import (
 	"fmt"
 	"math"
-	"strconv"
 	"unsafe"
 
+	"github.com/valyala/quicktemplate"
+
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 )
 
@@ -192,7 +193,7 @@ func (shard *pipeFormatProcessorShard) formatRow(pf *pipeFormat, br *blockResult
 			v := c.getValueAtRow(br, rowIdx)
 			switch step.fieldOpt {
 			case "q":
-				b = strconv.AppendQuote(b, v)
+				b = quicktemplate.AppendJSONString(b, v, true)
 			case "time":
 				nsecs, ok := tryParseInt64(v)
 				if !ok {
diff --git a/lib/logstorage/pipe_sort.go b/lib/logstorage/pipe_sort.go
index 849a721eb..07fa5d553 100644
--- a/lib/logstorage/pipe_sort.go
+++ b/lib/logstorage/pipe_sort.go
@@ -5,12 +5,13 @@ import (
 	"fmt"
 	"math"
 	"sort"
-	"strconv"
 	"strings"
 	"sync"
 	"sync/atomic"
 	"unsafe"
 
+	"github.com/valyala/quicktemplate"
+
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/stringsutil"
@@ -892,8 +893,8 @@ func tryParseInt64(s string) (int64, bool) {
 }
 
 func marshalJSONKeyValue(dst []byte, k, v string) []byte {
-	dst = strconv.AppendQuote(dst, k)
+	dst = quicktemplate.AppendJSONString(dst, k, true)
 	dst = append(dst, ':')
-	dst = strconv.AppendQuote(dst, v)
+	dst = quicktemplate.AppendJSONString(dst, v, true)
 	return dst
 }
diff --git a/lib/logstorage/rows.go b/lib/logstorage/rows.go
index 8b444a768..bee9fbd8d 100644
--- a/lib/logstorage/rows.go
+++ b/lib/logstorage/rows.go
@@ -2,7 +2,8 @@ package logstorage
 
 import (
 	"fmt"
-	"strconv"
+
+	"github.com/valyala/quicktemplate"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
@@ -62,9 +63,9 @@ func (f *Field) marshalToJSON(dst []byte) []byte {
 	if name == "" {
 		name = "_msg"
 	}
-	dst = strconv.AppendQuote(dst, name)
+	dst = quicktemplate.AppendJSONString(dst, name, true)
 	dst = append(dst, ':')
-	dst = strconv.AppendQuote(dst, f.Value)
+	dst = quicktemplate.AppendJSONString(dst, f.Value, true)
 	return dst
 }
 
@@ -72,7 +73,7 @@ func (f *Field) marshalToLogfmt(dst []byte) []byte {
 	dst = append(dst, f.Name...)
 	dst = append(dst, '=')
 	if needLogfmtQuoting(f.Value) {
-		dst = strconv.AppendQuote(dst, f.Value)
+		dst = quicktemplate.AppendJSONString(dst, f.Value, true)
 	} else {
 		dst = append(dst, f.Value...)
 	}
diff --git a/lib/logstorage/stats_uniq_values.go b/lib/logstorage/stats_uniq_values.go
index 4e07ab4fb..a266b2c02 100644
--- a/lib/logstorage/stats_uniq_values.go
+++ b/lib/logstorage/stats_uniq_values.go
@@ -3,10 +3,11 @@ package logstorage
 import (
 	"fmt"
 	"slices"
-	"strconv"
 	"strings"
 	"unsafe"
 
+	"github.com/valyala/quicktemplate"
+
 	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
 )
 
@@ -228,10 +229,10 @@ func marshalJSONArray(items []string) string {
 	b := make([]byte, 0, bufSize)
 
 	b = append(b, '[')
-	b = strconv.AppendQuote(b, items[0])
+	b = quicktemplate.AppendJSONString(b, items[0], true)
 	for _, item := range items[1:] {
 		b = append(b, ',')
-		b = strconv.AppendQuote(b, item)
+		b = quicktemplate.AppendJSONString(b, item, true)
 	}
 	b = append(b, ']')
 
diff --git a/vendor/github.com/valyala/quicktemplate/jsonstring.go b/vendor/github.com/valyala/quicktemplate/jsonstring.go
index 9154770da..e9aef8308 100644
--- a/vendor/github.com/valyala/quicktemplate/jsonstring.go
+++ b/vendor/github.com/valyala/quicktemplate/jsonstring.go
@@ -1,6 +1,7 @@
 package quicktemplate
 
 import (
+	"bytes"
 	"fmt"
 	"strings"
 )
@@ -17,7 +18,10 @@ func hasSpecialChars(s string) bool {
 	return false
 }
 
-func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
+// AppendJSONString appends json-encoded string s to dst and returns the result.
+//
+// If addQuotes is true, then the appended json string is wrapped into double quotes.
+func AppendJSONString(dst []byte, s string, addQuotes bool) []byte {
 	if !hasSpecialChars(s) {
 		// Fast path - nothing to escape.
 		if !addQuotes {
@@ -33,33 +37,64 @@ func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
 	if addQuotes {
 		dst = append(dst, '"')
 	}
-	bb := AcquireByteBuffer()
-	var tmp []byte
-	tmp, bb.B = bb.B, dst
-	_, err := jsonReplacer.WriteString(bb, s)
-	if err != nil {
-		panic(fmt.Errorf("BUG: unexpected error returned from jsonReplacer.WriteString: %s", err))
-	}
-	dst, bb.B = bb.B, tmp
-	ReleaseByteBuffer(bb)
+	dst = jsonReplacer.AppendReplace(dst, s)
 	if addQuotes {
 		dst = append(dst, '"')
 	}
 	return dst
 }
 
-var jsonReplacer = strings.NewReplacer(func() []string {
-	a := []string{
-		"\n", `\n`,
-		"\r", `\r`,
-		"\t", `\t`,
-		"\"", `\"`,
-		"\\", `\\`,
-		"<", `\u003c`,
-		"'", `\u0027`,
-	}
+var jsonReplacer = newByteReplacer(func() ([]byte, []string) {
+	oldChars := []byte("\n\r\t\b\f\"\\<'")
+	newStrings := []string{`\n`, `\r`, `\t`, `\b`, `\f`, `\"`, `\\`, `\u003c`, `\u0027`}
 	for i := 0; i < 0x20; i++ {
-		a = append(a, string([]byte{byte(i)}), fmt.Sprintf(`\u%04x`, i))
+		c := byte(i)
+		if n := bytes.IndexByte(oldChars, c); n >= 0 {
+			continue
+		}
+		oldChars = append(oldChars, byte(i))
+		newStrings = append(newStrings, fmt.Sprintf(`\u%04x`, i))
+	}
+	return oldChars, newStrings
+}())
+
+type byteReplacer struct {
+	m   [256]byte
+	newStrings []string
+}
+
+func newByteReplacer(oldChars []byte, newStrings []string) *byteReplacer {
+	if len(oldChars) != len(newStrings) {
+		panic(fmt.Errorf("len(oldChars)=%d must be equal to len(newStrings)=%d", len(oldChars), len(newStrings)))
+	}
+	if len(oldChars) >= 255 {
+		panic(fmt.Errorf("len(oldChars)=%d must be smaller than 255", len(oldChars)))
+	}
+
+	var m [256]byte
+	for i := range m[:] {
+		m[i] = 255
 	}
-	return a
-}()...)
+	for i, c := range oldChars {
+		m[c] = byte(i)
+	}
+	return &byteReplacer{
+		m:   m,
+		newStrings: newStrings,
+	}
+}
+
+func (br *byteReplacer) AppendReplace(dst []byte, s string) []byte {
+	m := br.m
+	newStrings := br.newStrings
+	for i := 0; i < len(s); i++ {
+		c := s[i]
+		n := m[c]
+		if n == 255 {
+			dst = append(dst, c)
+		} else {
+			dst = append(dst, newStrings[n]...)
+		}
+	}
+	return dst
+}
diff --git a/vendor/github.com/valyala/quicktemplate/util_appengine.go b/vendor/github.com/valyala/quicktemplate/util_appengine.go
index 7642bf7df..2b8f66d07 100644
--- a/vendor/github.com/valyala/quicktemplate/util_appengine.go
+++ b/vendor/github.com/valyala/quicktemplate/util_appengine.go
@@ -1,3 +1,4 @@
+//go:build appengine || appenginevm
 // +build appengine appenginevm
 
 package quicktemplate
diff --git a/vendor/github.com/valyala/quicktemplate/util_noappengine.go b/vendor/github.com/valyala/quicktemplate/util_noappengine.go
index f4da7ff6f..a288c50c7 100644
--- a/vendor/github.com/valyala/quicktemplate/util_noappengine.go
+++ b/vendor/github.com/valyala/quicktemplate/util_noappengine.go
@@ -1,3 +1,4 @@
+//go:build !appengine && !appenginevm
 // +build !appengine,!appenginevm
 
 package quicktemplate
diff --git a/vendor/github.com/valyala/quicktemplate/writer.go b/vendor/github.com/valyala/quicktemplate/writer.go
index b541dfdd0..283c78688 100644
--- a/vendor/github.com/valyala/quicktemplate/writer.go
+++ b/vendor/github.com/valyala/quicktemplate/writer.go
@@ -164,9 +164,9 @@ func (w *QWriter) FPrec(f float64, prec int) {
 func (w *QWriter) Q(s string) {
 	bb, ok := w.w.(*ByteBuffer)
 	if ok {
-		bb.B = appendJSONString(bb.B, s, true)
+		bb.B = AppendJSONString(bb.B, s, true)
 	} else {
-		w.b = appendJSONString(w.b[:0], s, true)
+		w.b = AppendJSONString(w.b[:0], s, true)
 		w.Write(w.b)
 	}
 }
@@ -184,9 +184,9 @@ func (w *QWriter) QZ(z []byte) {
 func (w *QWriter) J(s string) {
 	bb, ok := w.w.(*ByteBuffer)
 	if ok {
-		bb.B = appendJSONString(bb.B, s, false)
+		bb.B = AppendJSONString(bb.B, s, false)
 	} else {
-		w.b = appendJSONString(w.b[:0], s, false)
+		w.b = AppendJSONString(w.b[:0], s, false)
 		w.Write(w.b)
 	}
 }
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 982468ea9..3629c3a10 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -577,8 +577,8 @@ github.com/valyala/gozstd
 # github.com/valyala/histogram v1.2.0
 ## explicit; go 1.12
 github.com/valyala/histogram
-# github.com/valyala/quicktemplate v1.7.0
-## explicit; go 1.11
+# github.com/valyala/quicktemplate v1.8.0
+## explicit; go 1.17
 github.com/valyala/quicktemplate
 # github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1
 ## explicit; go 1.15


Test patch:
diff --git a/lib/logstorage/rows_test.go b/lib/logstorage/rows_test.go
index 8ddae6fce..e69a3c69a 100644
--- a/lib/logstorage/rows_test.go
+++ b/lib/logstorage/rows_test.go
@@ -5,6 +5,70 @@ import (
 	"testing"
 )
 
+func TestMarshalFieldsToJSON(t *testing.T) {
+	f := func(fields []Field, resultExpected string) {
+		t.Helper()
+
+		result := MarshalFieldsToJSON(nil, fields)
+		if string(result) != resultExpected {
+			t.Fatalf("unexpected result\ngot\n%q\nwant\n%q", result, resultExpected)
+		}
+	}
+
+	f(nil, "{}")
+	f([]Field{}, "{}")
+
+	f([]Field{
+		{
+			Name:  "foo",
+			Value: "bar",
+		},
+	}, `{"foo":"bar"}`)
+
+	f([]Field{
+		{
+			Name:  "foo\nbar",
+			Value: "  \u001b[32m ",
+		},
+		{
+			Name:  "  \u001b[11m ",
+			Value: "АБв",
+		},
+	}, `{"foo\nbar":"  \u001b[32m ","  \u001b[11m ":"АБв"}`)
+}
+
+func TestMarshalFieldsToLogfmt(t *testing.T) {
+	f := func(fields []Field, resultExpected string) {
+		t.Helper()
+
+		result := MarshalFieldsToLogfmt(nil, fields)
+		if string(result) != resultExpected {
+			t.Fatalf("unexpected result\ngot\n%q\nwant\n%q", result, resultExpected)
+		}
+	}
+
+	f(nil, "")
+	f([]Field{}, "")
+
+	f([]Field{
+		{
+			Name:  "foo",
+			Value: "bar",
+		},
+	}, `foo=bar`)
+
+	f([]Field{
+		{
+			Name:  "foo",
+			Value: "  \u001b[32m ",
+		},
+		{
+			Name:  "bar",
+			Value: "АБв",
+		},
+	}, `foo="  \u001b[32m " bar=АБв`)
+}
+
 func TestGetRowsSizeBytes(t *testing.T) {
 	f := func(rows [][]Field, uncompressedSizeBytesExpected int) {
 		t.Helper()