Compare commits
329 Commits
func25/per
...
streamaggr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe9113b5b2 | ||
|
|
ec0abe736a | ||
|
|
6434fa2c4e | ||
|
|
6b9f57e5f7 | ||
|
|
7e53324f5d | ||
|
|
5ee3bc98d6 | ||
|
|
0204ce942d | ||
|
|
d656934d22 | ||
|
|
ac82b5aea6 | ||
|
|
e8c7d6373e | ||
|
|
b17fce3e4b | ||
|
|
7ecf68093f | ||
|
|
50487823ab | ||
|
|
05f6ea621d | ||
|
|
1cb32ee6c8 | ||
|
|
a1882a84fb | ||
|
|
7a538bbe78 | ||
|
|
d553d101b2 | ||
|
|
73b073e298 | ||
|
|
361afaec5b | ||
|
|
a710d43a20 | ||
|
|
f9c79eba30 | ||
|
|
8c50c38a80 | ||
|
|
965a33c893 | ||
|
|
c4fe23794a | ||
|
|
41e0bbb6d1 | ||
|
|
025eec2cb0 | ||
|
|
14e33d93ef | ||
|
|
51cd3ba02b | ||
|
|
423df09d7d | ||
|
|
36a86c3aaf | ||
|
|
064b9a6314 | ||
|
|
0f24078146 | ||
|
|
8aa144fa74 | ||
|
|
1892e357c3 | ||
|
|
2023f017b1 | ||
|
|
78c6fb0883 | ||
|
|
c4b2fdff70 | ||
|
|
192c07f76a | ||
|
|
98fcd95438 | ||
|
|
d6bafe31d3 | ||
|
|
bc65c9f399 | ||
|
|
635bdd130b | ||
|
|
d036063c78 | ||
|
|
aa6c237603 | ||
|
|
05ac508fbf | ||
|
|
f0d1db81dc | ||
|
|
ab0d31a7b0 | ||
|
|
ca787c70d1 | ||
|
|
65e9d19f3c | ||
|
|
23f8ab6f81 | ||
|
|
3538869942 | ||
|
|
4984e71da6 | ||
|
|
c90adf566e | ||
|
|
c5fb281019 | ||
|
|
a72e1155b9 | ||
|
|
677f1cd1be | ||
|
|
9187ed0648 | ||
|
|
6ca1b15134 | ||
|
|
91987763d4 | ||
|
|
a23aa87282 | ||
|
|
508e498ae3 | ||
|
|
72941eac36 | ||
|
|
202eb429a7 | ||
|
|
1d637667a6 | ||
|
|
87910e4fa8 | ||
|
|
e347d90531 | ||
|
|
86029de0d4 | ||
|
|
0ff17c3ec4 | ||
|
|
6c9772b101 | ||
|
|
a8d8987825 | ||
|
|
daa7183749 | ||
|
|
bac193e50b | ||
|
|
343463fc0f | ||
|
|
41e0b62099 | ||
|
|
3c73dbbacc | ||
|
|
b4b79a4961 | ||
|
|
507b206a7d | ||
|
|
279e25e7c8 | ||
|
|
91f5417572 | ||
|
|
9e48074b59 | ||
|
|
200d723b9a | ||
|
|
867f671cc4 | ||
|
|
2239f5829f | ||
|
|
22d3f67908 | ||
|
|
d3f110373c | ||
|
|
c7771b1866 | ||
|
|
6a738e0b41 | ||
|
|
9eb0c1fd86 | ||
|
|
8fe41b2b08 | ||
|
|
95de37de2c | ||
|
|
b9a8c1ff3a | ||
|
|
595298ac98 | ||
|
|
f060b67da5 | ||
|
|
d3f4b01001 | ||
|
|
c910c1c6b8 | ||
|
|
ca2a08eabe | ||
|
|
fe022ed795 | ||
|
|
baa87b5b36 | ||
|
|
7b475ed95d | ||
|
|
252aa792f7 | ||
|
|
9413b2de91 | ||
|
|
6acf543b90 | ||
|
|
ad5d8097da | ||
|
|
e31625e0b2 | ||
|
|
344d61da79 | ||
|
|
f61d8c3ebb | ||
|
|
008b649658 | ||
|
|
e2c73dc89f | ||
|
|
03862368b5 | ||
|
|
6878982c93 | ||
|
|
eefae85450 | ||
|
|
9b6efb5e81 | ||
|
|
89686094a0 | ||
|
|
462b7cd597 | ||
|
|
ebd393d8b3 | ||
|
|
5481fa669c | ||
|
|
5e7b3e035b | ||
|
|
492190885d | ||
|
|
e144a2b062 | ||
|
|
2d8785fdf6 | ||
|
|
6c9e643ea8 | ||
|
|
2593f32b63 | ||
|
|
daad96b3a5 | ||
|
|
18dd4105be | ||
|
|
5c9bd35eb9 | ||
|
|
b2e7b05918 | ||
|
|
596e4de248 | ||
|
|
4d9ad9654f | ||
|
|
66645c3dff | ||
|
|
155089afbf | ||
|
|
c1cd3e85a7 | ||
|
|
364f084b43 | ||
|
|
63a9fb34d1 | ||
|
|
3f2bfd2ff6 | ||
|
|
7f47713821 | ||
|
|
c22eae0384 | ||
|
|
4086cef01c | ||
|
|
80d4acf2cf | ||
|
|
feba481ac2 | ||
|
|
ce81a86fc2 | ||
|
|
41850995d3 | ||
|
|
ddfb6db8cf | ||
|
|
5df015b9db | ||
|
|
bd84f8a35d | ||
|
|
07902baa8e | ||
|
|
4c6b7ce6da | ||
|
|
8592fc3162 | ||
|
|
150ee902fd | ||
|
|
01bc28eda2 | ||
|
|
cd8a478a8d | ||
|
|
36acde1d11 | ||
|
|
0d4f4b8f7d | ||
|
|
c04c377d09 | ||
|
|
504da7d02b | ||
|
|
dde2a0cb25 | ||
|
|
0e54cfe350 | ||
|
|
456aeda605 | ||
|
|
3634fefc64 | ||
|
|
776c501cb2 | ||
|
|
076a1f84e1 | ||
|
|
496015aa0e | ||
|
|
b05fbee63d | ||
|
|
234c81754e | ||
|
|
d57d8b5e60 | ||
|
|
a350be48b6 | ||
|
|
630211cfed | ||
|
|
61ae077e43 | ||
|
|
ead3250b80 | ||
|
|
fbaa026ae6 | ||
|
|
0b2d3d7752 | ||
|
|
7bb8853a5c | ||
|
|
aafa9262c5 | ||
|
|
82482fca4b | ||
|
|
f5f04c903b | ||
|
|
664f337c70 | ||
|
|
0c0f013a60 | ||
|
|
85ea0f80fc | ||
|
|
758f42fc12 | ||
|
|
ed5da38ede | ||
|
|
80a3c410d4 | ||
|
|
2df2e9b92f | ||
|
|
3d01bc3fbe | ||
|
|
5e67e611b8 | ||
|
|
2047ad20ef | ||
|
|
45cfb6b526 | ||
|
|
1da4650143 | ||
|
|
60183c7c79 | ||
|
|
3bbb2aed72 | ||
|
|
b52862badf | ||
|
|
55eb321f77 | ||
|
|
94afcbd9a9 | ||
|
|
dfcab4a47f | ||
|
|
0b91452ca4 | ||
|
|
8772aea24b | ||
|
|
14a0396f53 | ||
|
|
96efe99eef | ||
|
|
806bc2ac58 | ||
|
|
7d7d7c03bc | ||
|
|
fd890a2771 | ||
|
|
e62b88b2e0 | ||
|
|
00912bfa0f | ||
|
|
59bc63ebc4 | ||
|
|
05a64a8c14 | ||
|
|
86c0eb816c | ||
|
|
58c69386c7 | ||
|
|
c8e23eefba | ||
|
|
25a9802ca4 | ||
|
|
8657d03433 | ||
|
|
09b309a82e | ||
|
|
76c1b0b8ea | ||
|
|
fbde238cdc | ||
|
|
c896bf340d | ||
|
|
2d26d3e3de | ||
|
|
ea3b20622a | ||
|
|
b670b0e9ff | ||
|
|
8077585303 | ||
|
|
9367a9a6a2 | ||
|
|
b49d1ea809 | ||
|
|
13cc4a2618 | ||
|
|
6f1fde24dc | ||
|
|
31117c66d5 | ||
|
|
f65b976eda | ||
|
|
b82bd0c2ec | ||
|
|
3646724c6f | ||
|
|
4b1611267f | ||
|
|
037652d5ae | ||
|
|
6b775ca68c | ||
|
|
7c86835f3c | ||
|
|
7185fe012b | ||
|
|
2ec0cfec62 | ||
|
|
c6b2cac892 | ||
|
|
255d1d4e13 | ||
|
|
c89a7a0b62 | ||
|
|
c66da8b0ba | ||
|
|
e9950f6307 | ||
|
|
65b93b17b1 | ||
|
|
4599429f51 | ||
|
|
f934f71708 | ||
|
|
e75ae1b274 | ||
|
|
612be0954c | ||
|
|
6b1b47df54 | ||
|
|
7f1ba18719 | ||
|
|
cf2e7d0d92 | ||
|
|
f86e093b20 | ||
|
|
919d2dc90e | ||
|
|
9a0f697622 | ||
|
|
e28265fa39 | ||
|
|
8bb3f2fd43 | ||
|
|
c7569dac50 | ||
|
|
5319acb8ed | ||
|
|
cbeb7d50e8 | ||
|
|
df665a13c9 | ||
|
|
fea4433362 | ||
|
|
91b28d0527 | ||
|
|
524579d9bd | ||
|
|
a5c002edef | ||
|
|
4d0b41e63b | ||
|
|
109772bdc4 | ||
|
|
3964889705 | ||
|
|
3ed172eeeb | ||
|
|
78ecfede06 | ||
|
|
57183c9b61 | ||
|
|
4dc85613a2 | ||
|
|
0ada781cf2 | ||
|
|
8ed9978591 | ||
|
|
55febc0920 | ||
|
|
a2ecba4154 | ||
|
|
8645438a79 | ||
|
|
9e9583357d | ||
|
|
75a29655f5 | ||
|
|
5708a85499 | ||
|
|
787b9cd9a0 | ||
|
|
6f61e9d49d | ||
|
|
218c533874 | ||
|
|
7596e239eb | ||
|
|
36d58588a7 | ||
|
|
cd384c7547 | ||
|
|
d6d02d7aeb | ||
|
|
6167bccc5a | ||
|
|
59281d5358 | ||
|
|
6726a5aaed | ||
|
|
258201af04 | ||
|
|
a3d8077959 | ||
|
|
c00b64726c | ||
|
|
61721303fd | ||
|
|
4e976f66f3 | ||
|
|
69b976f08e | ||
|
|
b0bdb92729 | ||
|
|
e115b85770 | ||
|
|
7491f49e9e | ||
|
|
bc9cb69170 | ||
|
|
75cc7922c3 | ||
|
|
e86891b010 | ||
|
|
b82e2cabc5 | ||
|
|
8bc30b68cd | ||
|
|
4c228f1e18 | ||
|
|
d8f8822fa5 | ||
|
|
264c2ec6bd | ||
|
|
8207879fa3 | ||
|
|
e92f347336 | ||
|
|
c0272463d9 | ||
|
|
277fed9990 | ||
|
|
153cceb124 | ||
|
|
ae5f98f46b | ||
|
|
71df11dfbb | ||
|
|
47cd7bb4c1 | ||
|
|
ae4d376e41 | ||
|
|
f9a8c09fe8 | ||
|
|
7983ea4edb | ||
|
|
0ed835026f | ||
|
|
fb339d7574 | ||
|
|
c6faab77f9 | ||
|
|
6e4d29fc03 | ||
|
|
563a50b547 | ||
|
|
4fbdde5852 | ||
|
|
657988ac3a | ||
|
|
45a3713bdb | ||
|
|
eaee2d7db4 | ||
|
|
1cd06ace5a | ||
|
|
0a40064a6f | ||
|
|
c9bb4ddeed | ||
|
|
5261a84119 | ||
|
|
00e7d5add3 | ||
|
|
95acca6b52 | ||
|
|
feafb30266 | ||
|
|
a5424e95b3 | ||
|
|
9f7ee4c0bb | ||
|
|
0205170409 |
73
Makefile
@@ -27,6 +27,7 @@ include package/release/Makefile
|
||||
all: \
|
||||
victoria-metrics-prod \
|
||||
victoria-logs-prod \
|
||||
vlogscli-prod \
|
||||
vmagent-prod \
|
||||
vmalert-prod \
|
||||
vmalert-tool-prod \
|
||||
@@ -51,6 +52,7 @@ publish: \
|
||||
package: \
|
||||
package-victoria-metrics \
|
||||
package-victoria-logs \
|
||||
package-vlogscli \
|
||||
package-vmagent \
|
||||
package-vmalert \
|
||||
package-vmalert-tool \
|
||||
@@ -263,6 +265,14 @@ release-victoria-metrics-windows-goarch: victoria-metrics-windows-$(GOARCH)-prod
|
||||
cd bin && rm -rf \
|
||||
victoria-metrics-windows-$(GOARCH)-prod.exe
|
||||
|
||||
release-victoria-logs-bundle: \
|
||||
release-victoria-logs \
|
||||
release-vlogscli
|
||||
|
||||
publish-victoria-logs-bundle: \
|
||||
publish-victoria-logs \
|
||||
publish-vlogscli
|
||||
|
||||
release-victoria-logs:
|
||||
$(MAKE_PARALLEL) release-victoria-logs-linux-386 \
|
||||
release-victoria-logs-linux-amd64 \
|
||||
@@ -320,6 +330,63 @@ release-victoria-logs-windows-goarch: victoria-logs-windows-$(GOARCH)-prod
|
||||
cd bin && rm -rf \
|
||||
victoria-logs-windows-$(GOARCH)-prod.exe
|
||||
|
||||
release-vlogscli:
|
||||
$(MAKE_PARALLEL) release-vlogscli-linux-386 \
|
||||
release-vlogscli-linux-amd64 \
|
||||
release-vlogscli-linux-arm \
|
||||
release-vlogscli-linux-arm64 \
|
||||
release-vlogscli-darwin-amd64 \
|
||||
release-vlogscli-darwin-arm64 \
|
||||
release-vlogscli-freebsd-amd64 \
|
||||
release-vlogscli-openbsd-amd64 \
|
||||
release-vlogscli-windows-amd64
|
||||
|
||||
release-vlogscli-linux-386:
|
||||
GOOS=linux GOARCH=386 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-linux-amd64:
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-linux-arm:
|
||||
GOOS=linux GOARCH=arm $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-linux-arm64:
|
||||
GOOS=linux GOARCH=arm64 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-darwin-amd64:
|
||||
GOOS=darwin GOARCH=amd64 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-darwin-arm64:
|
||||
GOOS=darwin GOARCH=arm64 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-freebsd-amd64:
|
||||
GOOS=freebsd GOARCH=amd64 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-openbsd-amd64:
|
||||
GOOS=openbsd GOARCH=amd64 $(MAKE) release-vlogscli-goos-goarch
|
||||
|
||||
release-vlogscli-windows-amd64:
|
||||
GOARCH=amd64 $(MAKE) release-vlogscli-windows-goarch
|
||||
|
||||
release-vlogscli-goos-goarch: vlogscli-$(GOOS)-$(GOARCH)-prod
|
||||
cd bin && \
|
||||
tar $(TAR_OWNERSHIP) --transform="flags=r;s|-$(GOOS)-$(GOARCH)||" -czf vlogscli-$(GOOS)-$(GOARCH)-$(PKG_TAG).tar.gz \
|
||||
vlogscli-$(GOOS)-$(GOARCH)-prod \
|
||||
&& sha256sum vlogscli-$(GOOS)-$(GOARCH)-$(PKG_TAG).tar.gz \
|
||||
vlogscli-$(GOOS)-$(GOARCH)-prod \
|
||||
| sed s/-$(GOOS)-$(GOARCH)-prod/-prod/ > vlogscli-$(GOOS)-$(GOARCH)-$(PKG_TAG)_checksums.txt
|
||||
cd bin && rm -rf vlogscli-$(GOOS)-$(GOARCH)-prod
|
||||
|
||||
release-vlogscli-windows-goarch: vlogscli-windows-$(GOARCH)-prod
|
||||
cd bin && \
|
||||
zip vlogscli-windows-$(GOARCH)-$(PKG_TAG).zip \
|
||||
vlogscli-windows-$(GOARCH)-prod.exe \
|
||||
&& sha256sum vlogscli-windows-$(GOARCH)-$(PKG_TAG).zip \
|
||||
vlogscli-windows-$(GOARCH)-prod.exe \
|
||||
> vlogscli-windows-$(GOARCH)-$(PKG_TAG)_checksums.txt
|
||||
cd bin && rm -rf \
|
||||
vlogscli-windows-$(GOARCH)-prod.exe
|
||||
|
||||
release-vmutils: \
|
||||
release-vmutils-linux-386 \
|
||||
release-vmutils-linux-amd64 \
|
||||
@@ -467,9 +534,9 @@ benchmark-pure:
|
||||
CGO_ENABLED=0 go test -bench=. ./app/...
|
||||
|
||||
vendor-update:
|
||||
go get -u -d ./lib/...
|
||||
go get -u -d ./app/...
|
||||
go mod tidy -compat=1.22
|
||||
go get -u ./lib/...
|
||||
go get -u ./app/...
|
||||
go mod tidy -compat=1.23
|
||||
go mod vendor
|
||||
|
||||
app-local:
|
||||
|
||||
61
README.md
@@ -22,7 +22,7 @@ Here are some resources and information about VictoriaMetrics:
|
||||
- Case studies: [Grammarly, Roblox, Wix,...](https://docs.victoriametrics.com/casestudies/).
|
||||
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
||||
- Deployment types: [Single-node version](https://docs.victoriametrics.com/), [Cluster version](https://docs.victoriametrics.com/cluster-victoriametrics/), and [Enterprise version](https://docs.victoriametrics.com/enterprise/)
|
||||
- Changelog: [CHANGELOG](https://docs.victoriametrics.com/changelog/), and [How to upgrade](#how-to-upgrade-victoriametrics)
|
||||
- Changelog: [CHANGELOG](https://docs.victoriametrics.com/changelog/), and [How to upgrade](https://docs.victoriametrics.com/#how-to-upgrade-victoriametrics)
|
||||
- Community: [Slack](https://slack.victoriametrics.com/), [Twitter](https://twitter.com/VictoriaMetrics), [LinkedIn](https://www.linkedin.com/company/victoriametrics/), [YouTube](https://www.youtube.com/@VictoriaMetrics)
|
||||
|
||||
Yes, we open-source both the single-node VictoriaMetrics and the cluster version.
|
||||
@@ -38,17 +38,17 @@ VictoriaMetrics is optimized for timeseries data, even when old time series are
|
||||
* **Easy to setup**: No dependencies, single [small binary](https://medium.com/@valyala/stripping-dependency-bloat-in-victoriametrics-docker-image-983fb5912b0d), configuration through command-line flags, but the default is also fine-tuned; backup and restore with [instant snapshots](https://medium.com/@valyala/how-victoriametrics-makes-instant-snapshots-for-multi-terabyte-time-series-data-e1f3fb0e0282).
|
||||
* **Global query view**: Multiple Prometheus instances or any other data sources may ingest data into VictoriaMetrics and queried via a single query.
|
||||
* **Various Protocols**: Support metric scraping, ingestion and backfilling in various protocol.
|
||||
* [Prometheus exporters](#how-to-scrape-prometheus-exporters-such-as-node-exporter), [Prometheus remote write API](#prometheus-setup), [Prometheus exposition format](#how-to-import-data-in-prometheus-exposition-format).
|
||||
* [InfluxDB line protocol](#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) over HTTP, TCP and UDP.
|
||||
* [Graphite plaintext protocol](#how-to-send-data-from-graphite-compatible-agents-such-as-statsd) with [tags](https://graphite.readthedocs.io/en/latest/tags.html#carbon).
|
||||
* [OpenTSDB put message](#sending-data-via-telnet-put-protocol).
|
||||
* [HTTP OpenTSDB /api/put requests](#sending-opentsdb-data-via-http-apiput-requests).
|
||||
* [JSON line format](#how-to-import-data-in-json-line-format).
|
||||
* [Arbitrary CSV data](#how-to-import-csv-data).
|
||||
* [Native binary format](#how-to-import-data-in-native-format).
|
||||
* [DataDog agent or DogStatsD](#how-to-send-data-from-datadog-agent).
|
||||
* [NewRelic infrastructure agent](#how-to-send-data-from-newrelic-agent).
|
||||
* [OpenTelemetry metrics format](#sending-data-via-opentelemetry).
|
||||
* [Prometheus exporters](https://docs.victoriametrics.com/#how-to-scrape-prometheus-exporters-such-as-node-exporter), [Prometheus remote write API](https://docs.victoriametrics.com/#prometheus-setup), [Prometheus exposition format](https://docs.victoriametrics.com/#how-to-import-data-in-prometheus-exposition-format).
|
||||
* [InfluxDB line protocol](https://docs.victoriametrics.com/#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf) over HTTP, TCP and UDP.
|
||||
* [Graphite plaintext protocol](https://docs.victoriametrics.com/#how-to-send-data-from-graphite-compatible-agents-such-as-statsd) with [tags](https://graphite.readthedocs.io/en/latest/tags.html#carbon).
|
||||
* [OpenTSDB put message](https://docs.victoriametrics.com/#sending-data-via-telnet-put-protocol).
|
||||
* [HTTP OpenTSDB /api/put requests](https://docs.victoriametrics.com/#sending-opentsdb-data-via-http-apiput-requests).
|
||||
* [JSON line format](https://docs.victoriametrics.com/#how-to-import-data-in-json-line-format).
|
||||
* [Arbitrary CSV data](https://docs.victoriametrics.com/#how-to-import-csv-data).
|
||||
* [Native binary format](https://docs.victoriametrics.com/#how-to-import-data-in-native-format).
|
||||
* [DataDog agent or DogStatsD](https://docs.victoriametrics.com/#how-to-send-data-from-datadog-agent).
|
||||
* [NewRelic infrastructure agent](https://docs.victoriametrics.com/#how-to-send-data-from-newrelic-agent).
|
||||
* [OpenTelemetry metrics format](https://docs.victoriametrics.com/#sending-data-via-opentelemetry).
|
||||
* **NFS-based storages**: Supports storing data on NFS-based storages such as Amazon EFS, Google Filestore.
|
||||
* And many other features such as metrics relabeling, cardinality limiter, etc.
|
||||
|
||||
@@ -95,30 +95,31 @@ If you like VictoriaMetrics and want to contribute, then please [read these docs
|
||||
|
||||
## VictoriaMetrics Logo
|
||||
|
||||
[Zip](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/VM_logo.zip) contains three folders with different image orientations (main color and inverted version).
|
||||
The provided [ZIP file](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/VM_logo.zip) contains three folders with different logo orientations. Each folder includes the following file types:
|
||||
|
||||
Files included in each folder:
|
||||
* JPEG: Preview files
|
||||
* PNG: Preview files with transparent background
|
||||
* AI: Adobe Illustrator files
|
||||
|
||||
* 2 JPEG Preview files
|
||||
* 2 PNG Preview files with transparent background
|
||||
* 2 EPS Adobe Illustrator EPS10 files
|
||||
### VictoriaMetrics Logo Usage Guidelines
|
||||
|
||||
### Logo Usage Guidelines
|
||||
#### Font
|
||||
|
||||
#### Font used
|
||||
|
||||
* Lato Black
|
||||
* Lato Regular
|
||||
* Font Used: Lato Black
|
||||
* Download here: [Lato Font](https://fonts.google.com/specimen/Lato)
|
||||
|
||||
#### Color Palette
|
||||
|
||||
* HEX [#110f0f](https://www.color-hex.com/color/110f0f)
|
||||
* HEX [#ffffff](https://www.color-hex.com/color/ffffff)
|
||||
* Black [#000000](https://www.color-hex.com/color/000000)
|
||||
* Purple [#4d0e82](https://www.color-hex.com/color/4d0e82)
|
||||
* Orange [#ff2e00](https://www.color-hex.com/color/ff2e00)
|
||||
* White [#ffffff](https://www.color-hex.com/color/ffffff)
|
||||
|
||||
### We kindly ask
|
||||
### Logo Usage Rules
|
||||
|
||||
* Please don't use any other font instead of suggested.
|
||||
* To keep enough clear space around the logo.
|
||||
* Do not change spacing, alignment, or relative locations of the design elements.
|
||||
* Do not change the proportions for any of the design elements or the design itself.
|
||||
You may resize as needed but must retain all proportions.
|
||||
* Only use the Lato Black font as specified.
|
||||
* Maintain sufficient clear space around the logo for visibility.
|
||||
* Do not modify the spacing, alignment, or positioning of design elements.
|
||||
* You may resize the logo as needed, but ensure all proportions remain intact.
|
||||
|
||||
Thank you for your cooperation!
|
||||
BIN
VM_logo.zip
@@ -92,6 +92,9 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
if vlselect.RequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
if vlstorage.RequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@ FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 9428
|
||||
ENTRYPOINT ["/victoria-logs-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY victoria-logs-linux-${TARGETARCH}-prod ./victoria-logs-prod
|
||||
|
||||
@@ -8,5 +8,5 @@ FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 8428
|
||||
ENTRYPOINT ["/victoria-metrics-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY victoria-metrics-linux-${TARGETARCH}-prod ./victoria-metrics-prod
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -251,19 +249,8 @@ func parseElasticsearchTimestamp(s string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
if len(s) < len("YYYY-MM-DD") || s[len("YYYY")] != '-' {
|
||||
// Try parsing timestamp in milliseconds
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse timestamp in milliseconds from %q: %w", s, err)
|
||||
}
|
||||
if n > int64(math.MaxInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too big timestamp in milliseconds: %d; mustn't exceed %d", n, int64(math.MaxInt64)/1e6)
|
||||
}
|
||||
if n < int64(math.MinInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too small timestamp in milliseconds: %d; must be bigger than %d", n, int64(math.MinInt64)/1e6)
|
||||
}
|
||||
n *= 1e6
|
||||
return n, nil
|
||||
// Try parsing timestamp in seconds or milliseconds
|
||||
return insertutils.ParseUnixTimestamp(s)
|
||||
}
|
||||
if len(s) == len("YYYY-MM-DD") {
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
|
||||
@@ -76,17 +76,20 @@ func TestReadBulkRequest_Success(t *testing.T) {
|
||||
data := `{"create":{"_index":"filebeat-8.8.0"}}
|
||||
{"@timestamp":"2023-06-06T04:48:11.735Z","log":{"offset":71770,"file":{"path":"/var/log/auth.log"}},"message":"foobar"}
|
||||
{"create":{"_index":"filebeat-8.8.0"}}
|
||||
{"@timestamp":"2023-06-06T04:48:12.735Z","message":"baz"}
|
||||
{"@timestamp":"2023-06-06 04:48:12.735+01:00","message":"baz"}
|
||||
{"index":{"_index":"filebeat-8.8.0"}}
|
||||
{"message":"xyz","@timestamp":"2023-06-06T04:48:13.735Z","x":"y"}
|
||||
{"message":"xyz","@timestamp":"1686026893735","x":"y"}
|
||||
{"create":{"_index":"filebeat-8.8.0"}}
|
||||
{"message":"qwe rty","@timestamp":"1686026893"}
|
||||
`
|
||||
timeField := "@timestamp"
|
||||
msgField := "message"
|
||||
rowsExpected := 3
|
||||
timestampsExpected := []int64{1686026891735000000, 1686026892735000000, 1686026893735000000}
|
||||
resultExpected := `{"@timestamp":"","log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
|
||||
{"@timestamp":"","_msg":"baz"}
|
||||
{"_msg":"xyz","@timestamp":"","x":"y"}`
|
||||
rowsExpected := 4
|
||||
timestampsExpected := []int64{1686026891735000000, 1686023292735000000, 1686026893735000000, 1686026893000000000}
|
||||
resultExpected := `{"log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
|
||||
{"_msg":"baz"}
|
||||
{"_msg":"xyz","x":"y"}
|
||||
{"_msg":"qwe rty"}`
|
||||
f(data, timeField, msgField, rowsExpected, timestampsExpected, resultExpected)
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,22 @@ func (lmp *logMessageProcessor) AddRow(timestamp int64, fields []logstorage.Fiel
|
||||
return
|
||||
}
|
||||
|
||||
// _msg field must be non-empty according to VictoriaLogs data model.
|
||||
// See https://docs.victoriametrics.com/victorialogs/keyconcepts/#message-field
|
||||
msgExist := false
|
||||
for i := range fields {
|
||||
if fields[i].Name == "_msg" {
|
||||
msgExist = len(fields[i].Value) > 0
|
||||
break
|
||||
}
|
||||
}
|
||||
if !msgExist {
|
||||
rf := logstorage.RowFormatter(fields)
|
||||
logger.Warnf("dropping log line without _msg field; %s", rf)
|
||||
rowsDroppedTotalMsgNotValid.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
lmp.lr.MustAdd(lmp.cp.TenantID, timestamp, fields)
|
||||
if lmp.cp.Debug {
|
||||
s := lmp.lr.GetRowString(0)
|
||||
@@ -225,4 +241,5 @@ func (cp *CommonParams) NewLogMessageProcessor() LogMessageProcessor {
|
||||
var (
|
||||
rowsDroppedTotalDebug = metrics.NewCounter(`vl_rows_dropped_total{reason="debug"}`)
|
||||
rowsDroppedTotalTooManyFields = metrics.NewCounter(`vl_rows_dropped_total{reason="too_many_fields"}`)
|
||||
rowsDroppedTotalMsgNotValid = metrics.NewCounter(`vl_rows_dropped_total{reason="msg_not_exist"}`)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ package insertutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
@@ -19,15 +21,49 @@ func ExtractTimestampRFC3339NanoFromFields(timeField string, fields []logstorage
|
||||
if f.Name != timeField {
|
||||
continue
|
||||
}
|
||||
if f.Value == "" || f.Value == "0" {
|
||||
return time.Now().UnixNano(), nil
|
||||
}
|
||||
nsecs, ok := logstorage.TryParseTimestampRFC3339Nano(f.Value)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("cannot unmarshal rfc3339 timestamp from %s=%q", timeField, f.Value)
|
||||
nsecs, err := parseTimestamp(f.Value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse timestamp from field %q: %s", timeField, err)
|
||||
}
|
||||
f.Value = ""
|
||||
if nsecs == 0 {
|
||||
nsecs = time.Now().UnixNano()
|
||||
}
|
||||
return nsecs, nil
|
||||
}
|
||||
return time.Now().UnixNano(), nil
|
||||
}
|
||||
|
||||
func parseTimestamp(s string) (int64, error) {
|
||||
if s == "" || s == "0" {
|
||||
return time.Now().UnixNano(), nil
|
||||
}
|
||||
if len(s) <= len("YYYY") || s[len("YYYY")] != '-' {
|
||||
return ParseUnixTimestamp(s)
|
||||
}
|
||||
nsecs, ok := logstorage.TryParseTimestampRFC3339Nano(s)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("cannot unmarshal rfc3339 timestamp %q", s)
|
||||
}
|
||||
return nsecs, nil
|
||||
}
|
||||
|
||||
// ParseUnixTimestamp parses s as unix timestamp in either seconds or milliseconds and returns the parsed timestamp in nanoseconds.
|
||||
func ParseUnixTimestamp(s string) (int64, error) {
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse unix timestamp from %q: %w", s, err)
|
||||
}
|
||||
if n < (1<<31) && n >= (-1<<31) {
|
||||
// The timestamp is in seconds. Convert it to milliseconds
|
||||
n *= 1e3
|
||||
}
|
||||
if n > int64(math.MaxInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too big timestamp in milliseconds: %d; mustn't exceed %d", n, int64(math.MaxInt64)/1e6)
|
||||
}
|
||||
if n < int64(math.MinInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too small timestamp in milliseconds: %d; must be bigger than %d", n, int64(math.MinInt64)/1e6)
|
||||
}
|
||||
n *= 1e6
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -27,25 +27,41 @@ func TestExtractTimestampRFC3339NanoFromFields_Success(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// UTC time
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "2024-06-18T23:37:20Z"},
|
||||
}, 1718753840000000000)
|
||||
|
||||
// Time with timezone
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "2024-06-18T23:37:20+08:00"},
|
||||
}, 1718725040000000000)
|
||||
|
||||
// SQL datetime format
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "2024-06-18T23:37:20.123-05:30"},
|
||||
{Name: "time", Value: "2024-06-18 23:37:20.123-05:30"},
|
||||
}, 1718773640123000000)
|
||||
|
||||
// Time with nanosecond precision
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "time", Value: "2024-06-18T23:37:20.123456789-05:30"},
|
||||
{Name: "foo", Value: "bar"},
|
||||
}, 1718773640123456789)
|
||||
|
||||
// Unix timestamp in milliseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "1718773640123"},
|
||||
}, 1718773640123000000)
|
||||
|
||||
// Unix timestamp in seconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "1718773640"},
|
||||
}, 1718773640000000000)
|
||||
}
|
||||
|
||||
func TestExtractTimestampRFC3339NanoFromFields_Error(t *testing.T) {
|
||||
@@ -66,9 +82,6 @@ func TestExtractTimestampRFC3339NanoFromFields_Error(t *testing.T) {
|
||||
|
||||
f("foobar")
|
||||
|
||||
// no Z at the end
|
||||
f("2024-06-18T23:37:20")
|
||||
|
||||
// incomplete time
|
||||
f("2024-06-18")
|
||||
f("2024-06-18T23:37")
|
||||
|
||||
@@ -23,16 +23,16 @@ func TestProcessStreamInternal_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
data := `{"@timestamp":"2023-06-06T04:48:11.735Z","log":{"offset":71770,"file":{"path":"/var/log/auth.log"}},"message":"foobar"}
|
||||
{"@timestamp":"2023-06-06T04:48:12.735Z","message":"baz"}
|
||||
{"message":"xyz","@timestamp":"2023-06-06T04:48:13.735Z","x":"y"}
|
||||
{"@timestamp":"2023-06-06T04:48:12.735+01:00","message":"baz"}
|
||||
{"message":"xyz","@timestamp":"2023-06-06 04:48:13.735Z","x":"y"}
|
||||
`
|
||||
timeField := "@timestamp"
|
||||
msgField := "message"
|
||||
rowsExpected := 3
|
||||
timestampsExpected := []int64{1686026891735000000, 1686026892735000000, 1686026893735000000}
|
||||
resultExpected := `{"@timestamp":"","log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
|
||||
{"@timestamp":"","_msg":"baz"}
|
||||
{"_msg":"xyz","@timestamp":"","x":"y"}`
|
||||
timestampsExpected := []int64{1686026891735000000, 1686023292735000000, 1686026893735000000}
|
||||
resultExpected := `{"log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
|
||||
{"_msg":"baz"}
|
||||
{"_msg":"xyz","x":"y"}`
|
||||
f(data, timeField, msgField, rowsExpected, timestampsExpected, resultExpected)
|
||||
}
|
||||
|
||||
|
||||
@@ -101,9 +101,9 @@ func TestProcessStreamInternal_Success(t *testing.T) {
|
||||
currentYear := 2023
|
||||
rowsExpected := 3
|
||||
timestampsExpected := []int64{1685794113000000000, 1685880513000000000, 1685814132345000000}
|
||||
resultExpected := `{"format":"rfc3164","timestamp":"","hostname":"abcd","app_name":"systemd","_msg":"Starting Update the local ESM caches..."}
|
||||
{"priority":"165","facility":"20","severity":"5","format":"rfc3164","timestamp":"","hostname":"abcd","app_name":"systemd","proc_id":"345","_msg":"abc defg"}
|
||||
{"priority":"123","facility":"15","severity":"3","format":"rfc5424","timestamp":"","hostname":"mymachine.example.com","app_name":"appname","proc_id":"12345","msg_id":"ID47","exampleSDID@32473.iut":"3","exampleSDID@32473.eventSource":"Application 123 = ] 56","exampleSDID@32473.eventID":"11211","_msg":"This is a test message with structured data."}`
|
||||
resultExpected := `{"format":"rfc3164","hostname":"abcd","app_name":"systemd","_msg":"Starting Update the local ESM caches..."}
|
||||
{"priority":"165","facility":"20","severity":"5","format":"rfc3164","hostname":"abcd","app_name":"systemd","proc_id":"345","_msg":"abc defg"}
|
||||
{"priority":"123","facility":"15","severity":"3","format":"rfc5424","hostname":"mymachine.example.com","app_name":"appname","proc_id":"12345","msg_id":"ID47","exampleSDID@32473.iut":"3","exampleSDID@32473.eventSource":"Application 123 = ] 56","exampleSDID@32473.eventID":"11211","_msg":"This is a test message with structured data."}`
|
||||
f(data, currentYear, rowsExpected, timestampsExpected, resultExpected)
|
||||
}
|
||||
|
||||
|
||||
109
app/vlogscli/Makefile
Normal file
@@ -0,0 +1,109 @@
|
||||
# All these commands must run from repository root.
|
||||
|
||||
vlogscli:
|
||||
APP_NAME=vlogscli $(MAKE) app-local
|
||||
|
||||
vlogscli-race:
|
||||
APP_NAME=vlogscli RACE=-race $(MAKE) app-local
|
||||
|
||||
vlogscli-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker
|
||||
|
||||
vlogscli-pure-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-pure
|
||||
|
||||
vlogscli-linux-amd64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-linux-amd64
|
||||
|
||||
vlogscli-linux-arm-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-linux-arm
|
||||
|
||||
vlogscli-linux-arm64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-linux-arm64
|
||||
|
||||
vlogscli-linux-ppc64le-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-linux-ppc64le
|
||||
|
||||
vlogscli-linux-386-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-linux-386
|
||||
|
||||
vlogscli-darwin-amd64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-darwin-amd64
|
||||
|
||||
vlogscli-darwin-arm64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-darwin-arm64
|
||||
|
||||
vlogscli-freebsd-amd64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-freebsd-amd64
|
||||
|
||||
vlogscli-openbsd-amd64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-openbsd-amd64
|
||||
|
||||
vlogscli-windows-amd64-prod:
|
||||
APP_NAME=vlogscli $(MAKE) app-via-docker-windows-amd64
|
||||
|
||||
package-vlogscli:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker
|
||||
|
||||
package-vlogscli-pure:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker-pure
|
||||
|
||||
package-vlogscli-amd64:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker-amd64
|
||||
|
||||
package-vlogscli-arm:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker-arm
|
||||
|
||||
package-vlogscli-arm64:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker-arm64
|
||||
|
||||
package-vlogscli-ppc64le:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker-ppc64le
|
||||
|
||||
package-vlogscli-386:
|
||||
APP_NAME=vlogscli $(MAKE) package-via-docker-386
|
||||
|
||||
publish-vlogscli:
|
||||
APP_NAME=vlogscli $(MAKE) publish-via-docker
|
||||
|
||||
vlogscli-linux-amd64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=1 GOOS=linux GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-linux-arm:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=linux GOARCH=arm $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-linux-arm64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-linux-ppc64le:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-linux-s390x:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=linux GOARCH=s390x $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-linux-loong64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=linux GOARCH=loong64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-linux-386:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=linux GOARCH=386 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-darwin-amd64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-darwin-arm64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-freebsd-amd64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-openbsd-amd64:
|
||||
APP_NAME=vlogscli CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(MAKE) app-local-goos-goarch
|
||||
|
||||
vlogscli-windows-amd64:
|
||||
GOARCH=amd64 APP_NAME=vlogscli $(MAKE) app-local-windows-goarch
|
||||
|
||||
vlogscli-pure:
|
||||
APP_NAME=vlogscli $(MAKE) app-local-pure
|
||||
|
||||
run-vlogscli:
|
||||
APP_NAME=vlogscli $(MAKE) run-via-docker
|
||||
5
app/vlogscli/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# vlogscli
|
||||
|
||||
Command-line utility for querying [VictoriaLogs](https://docs.victoriametrics.com/victorialogs/).
|
||||
|
||||
See [these docs](https://docs.victoriametrics.com/victorialogs/querying/vlogscli/).
|
||||
6
app/vlogscli/deployment/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
ARG base_image=non-existing
|
||||
FROM $base_image
|
||||
|
||||
ENTRYPOINT ["/vlogscli-prod"]
|
||||
ARG src_binary=non-existing
|
||||
COPY $src_binary ./vlogscli-prod
|
||||
238
app/vlogscli/json_prettifier.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
)
|
||||
|
||||
type outputMode int
|
||||
|
||||
const (
|
||||
outputModeJSONMultiline = outputMode(0)
|
||||
outputModeJSONSingleline = outputMode(1)
|
||||
outputModeLogfmt = outputMode(2)
|
||||
outputModeCompact = outputMode(3)
|
||||
)
|
||||
|
||||
func getOutputFormatter(outputMode outputMode) func(w io.Writer, fields []logstorage.Field) error {
|
||||
switch outputMode {
|
||||
case outputModeJSONMultiline:
|
||||
return func(w io.Writer, fields []logstorage.Field) error {
|
||||
return writeJSONObject(w, fields, true)
|
||||
}
|
||||
case outputModeJSONSingleline:
|
||||
return func(w io.Writer, fields []logstorage.Field) error {
|
||||
return writeJSONObject(w, fields, false)
|
||||
}
|
||||
case outputModeLogfmt:
|
||||
return writeLogfmtObject
|
||||
case outputModeCompact:
|
||||
return writeCompactObject
|
||||
default:
|
||||
panic(fmt.Errorf("BUG: unexpected outputMode=%d", outputMode))
|
||||
}
|
||||
}
|
||||
|
||||
type jsonPrettifier struct {
|
||||
r io.ReadCloser
|
||||
formatter func(w io.Writer, fields []logstorage.Field) error
|
||||
|
||||
d *json.Decoder
|
||||
|
||||
pr *io.PipeReader
|
||||
pw *io.PipeWriter
|
||||
bw *bufio.Writer
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func newJSONPrettifier(r io.ReadCloser, outputMode outputMode) *jsonPrettifier {
|
||||
d := json.NewDecoder(r)
|
||||
pr, pw := io.Pipe()
|
||||
bw := bufio.NewWriter(pw)
|
||||
|
||||
formatter := getOutputFormatter(outputMode)
|
||||
|
||||
jp := &jsonPrettifier{
|
||||
r: r,
|
||||
formatter: formatter,
|
||||
|
||||
d: d,
|
||||
|
||||
pr: pr,
|
||||
pw: pw,
|
||||
bw: bw,
|
||||
}
|
||||
|
||||
jp.wg.Add(1)
|
||||
go func() {
|
||||
defer jp.wg.Done()
|
||||
err := jp.prettifyJSONLines()
|
||||
jp.closePipesWithError(err)
|
||||
}()
|
||||
|
||||
return jp
|
||||
}
|
||||
|
||||
func (jp *jsonPrettifier) closePipesWithError(err error) {
|
||||
_ = jp.pr.CloseWithError(err)
|
||||
_ = jp.pw.CloseWithError(err)
|
||||
}
|
||||
|
||||
func (jp *jsonPrettifier) prettifyJSONLines() error {
|
||||
for jp.d.More() {
|
||||
fields, err := readNextJSONObject(jp.d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
return fields[i].Name < fields[j].Name
|
||||
})
|
||||
if err := jp.formatter(jp.bw, fields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush bw after every output line in order to show results as soon as they appear.
|
||||
if err := jp.bw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (jp *jsonPrettifier) Close() error {
|
||||
jp.closePipesWithError(io.ErrUnexpectedEOF)
|
||||
err := jp.r.Close()
|
||||
jp.wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
func (jp *jsonPrettifier) Read(p []byte) (int, error) {
|
||||
return jp.pr.Read(p)
|
||||
}
|
||||
|
||||
func readNextJSONObject(d *json.Decoder) ([]logstorage.Field, error) {
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read '{': %w", err)
|
||||
}
|
||||
delim, ok := t.(json.Delim)
|
||||
if !ok || delim.String() != "{" {
|
||||
return nil, fmt.Errorf("unexpected token read; got %q; want '{'", delim)
|
||||
}
|
||||
|
||||
var fields []logstorage.Field
|
||||
for {
|
||||
// Read object key
|
||||
t, err := d.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read JSON object key or closing brace: %w", err)
|
||||
}
|
||||
delim, ok := t.(json.Delim)
|
||||
if ok {
|
||||
if delim.String() == "}" {
|
||||
return fields, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delimiter read; got %q; want '}'", delim)
|
||||
}
|
||||
key, ok := t.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected token read for object key: %v; want string or '}'", t)
|
||||
}
|
||||
|
||||
// read object value
|
||||
t, err = d.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read JSON object value: %w", err)
|
||||
}
|
||||
value, ok := t.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected token read for oject value: %v; want string", t)
|
||||
}
|
||||
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeLogfmtObject(w io.Writer, fields []logstorage.Field) error {
|
||||
data := logstorage.MarshalFieldsToLogfmt(nil, fields)
|
||||
_, err := fmt.Fprintf(w, "%s\n", data)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeCompactObject(w io.Writer, fields []logstorage.Field) error {
|
||||
if len(fields) == 1 {
|
||||
// Just write field value as is without name
|
||||
_, err := fmt.Fprintf(w, "%s\n", fields[0].Value)
|
||||
return err
|
||||
}
|
||||
if len(fields) == 2 && fields[0].Name == "_time" || fields[1].Name == "_time" {
|
||||
// Write _time\tfieldValue as is
|
||||
if fields[0].Name == "_time" {
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\n", fields[0].Value, fields[1].Value)
|
||||
return err
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\n", fields[1].Value, fields[0].Value)
|
||||
return err
|
||||
}
|
||||
|
||||
// Fall back to logfmt
|
||||
return writeLogfmtObject(w, fields)
|
||||
}
|
||||
|
||||
func writeJSONObject(w io.Writer, fields []logstorage.Field, isMultiline bool) error {
|
||||
if len(fields) == 0 {
|
||||
fmt.Fprintf(w, "{}\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "{")
|
||||
writeNewlineIfNeeded(w, isMultiline)
|
||||
if err := writeJSONObjectKeyValue(w, fields[0], isMultiline); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range fields[1:] {
|
||||
fmt.Fprintf(w, ",")
|
||||
writeNewlineIfNeeded(w, isMultiline)
|
||||
if err := writeJSONObjectKeyValue(w, f, isMultiline); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
writeNewlineIfNeeded(w, isMultiline)
|
||||
fmt.Fprintf(w, "}\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeNewlineIfNeeded(w io.Writer, isMultiline bool) {
|
||||
if isMultiline {
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONObjectKeyValue(w io.Writer, f logstorage.Field, isMultiline bool) error {
|
||||
key := getJSONString(f.Name)
|
||||
value := getJSONString(f.Value)
|
||||
if isMultiline {
|
||||
_, err := fmt.Fprintf(w, " %s: %s", key, value)
|
||||
return err
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s:%s", key, value)
|
||||
return err
|
||||
}
|
||||
|
||||
func getJSONString(s string) string {
|
||||
data, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unexpected error when marshaling string to JSON: %w", err))
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
116
app/vlogscli/less_wrapper.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
func isTerminal() bool {
|
||||
return isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stderr.Fd())
|
||||
}
|
||||
|
||||
func readWithLess(r io.Reader) error {
|
||||
if !isTerminal() {
|
||||
// Just write everything to stdout if no terminal is available.
|
||||
_, err := io.Copy(os.Stdout, r)
|
||||
if err != nil && !isErrPipe(err) {
|
||||
return fmt.Errorf("error when forwarding data to stdout: %w", err)
|
||||
}
|
||||
if err := os.Stdout.Sync(); err != nil {
|
||||
return fmt.Errorf("cannot sync data to stdout: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create pipe: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
}()
|
||||
|
||||
// Ignore Ctrl+C in the current process, so 'less' could handle it properly
|
||||
cancel := ignoreSignals(os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
// Start 'less' process
|
||||
path, err := exec.LookPath("less")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot find 'less' command: %w", err)
|
||||
}
|
||||
p, err := os.StartProcess(path, []string{"less", "-F", "-X"}, &os.ProcAttr{
|
||||
Env: append(os.Environ(), "LESSCHARSET=utf-8"),
|
||||
Files: []*os.File{pr, os.Stdout, os.Stderr},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start 'less' process: %w", err)
|
||||
}
|
||||
|
||||
// Close pr after 'less' finishes in a parallel goroutine
|
||||
// in order to unblock forwarding data to stopped 'less' below.
|
||||
waitch := make(chan *os.ProcessState)
|
||||
go func() {
|
||||
// Wait for 'less' process to finish.
|
||||
ps, err := p.Wait()
|
||||
if err != nil {
|
||||
fatalf("unexpected error when waiting for 'less' process: %w", err)
|
||||
}
|
||||
_ = pr.Close()
|
||||
waitch <- ps
|
||||
}()
|
||||
|
||||
// Forward data from r to 'less'
|
||||
_, err = io.Copy(pw, r)
|
||||
_ = pw.Sync()
|
||||
_ = pw.Close()
|
||||
|
||||
// Wait until 'less' finished
|
||||
ps := <-waitch
|
||||
|
||||
// Verify 'less' status.
|
||||
if !ps.Success() {
|
||||
return fmt.Errorf("'less' finished with unexpected code %d", ps.ExitCode())
|
||||
}
|
||||
|
||||
if err != nil && !isErrPipe(err) {
|
||||
return fmt.Errorf("error when forwarding data to 'less': %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isErrPipe(err error) bool {
|
||||
return errors.Is(err, syscall.EPIPE) || errors.Is(err, io.ErrClosedPipe)
|
||||
}
|
||||
|
||||
func ignoreSignals(sigs ...os.Signal) func() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, sigs...)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
_, ok := <-ch
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
signal.Stop(ch)
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
420
app/vlogscli/main.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/readline"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
)
|
||||
|
||||
var (
|
||||
datasourceURL = flag.String("datasource.url", "http://localhost:9428/select/logsql/query", "URL for querying VictoriaLogs; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/querying/#querying-logs . See also -tail.url")
|
||||
tailURL = flag.String("tail.url", "", "URL for live tailing queries to VictoriaLogs; see https://docs.victoriametrics.com/victorialogs/querying/#live-tailing ."+
|
||||
"The url is automatically detected from -datasource.url by replacing /query with /tail at the end if -tail.url is empty")
|
||||
historyFile = flag.String("historyFile", "vlogscli-history", "Path to file with command history")
|
||||
header = flagutil.NewArrayString("header", "Optional header to pass in request -datasource.url in the form 'HeaderName: value'")
|
||||
accountID = flag.Int("accountID", 0, "Account ID to query; see https://docs.victoriametrics.com/victorialogs/#multitenancy")
|
||||
projectID = flag.Int("projectID", 0, "Project ID to query; see https://docs.victoriametrics.com/victorialogs/#multitenancy")
|
||||
)
|
||||
|
||||
const (
|
||||
firstLinePrompt = ";> "
|
||||
nextLinePrompt = ""
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Write flags and help message to stdout, since it is easier to grep or pipe.
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
buildinfo.Init()
|
||||
logger.InitNoLogFlags()
|
||||
|
||||
hes, err := parseHeaders(*header)
|
||||
if err != nil {
|
||||
fatalf("cannot parse -header command-line flag: %s", err)
|
||||
}
|
||||
headers = hes
|
||||
|
||||
incompleteLine := ""
|
||||
cfg := &readline.Config{
|
||||
Prompt: firstLinePrompt,
|
||||
DisableAutoSaveHistory: true,
|
||||
Listener: func(line []rune, pos int, _ rune) ([]rune, int, bool) {
|
||||
incompleteLine = string(line)
|
||||
return line, pos, false
|
||||
},
|
||||
}
|
||||
rl, err := readline.NewFromConfig(cfg)
|
||||
if err != nil {
|
||||
fatalf("cannot initialize readline: %s", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(rl, "sending queries to %s\n", *datasourceURL)
|
||||
|
||||
runReadlineLoop(rl, &incompleteLine)
|
||||
|
||||
if err := rl.Close(); err != nil {
|
||||
fatalf("cannot close readline: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func runReadlineLoop(rl *readline.Instance, incompleteLine *string) {
|
||||
historyLines, err := loadFromHistory(*historyFile)
|
||||
if err != nil {
|
||||
fatalf("cannot load query history: %s", err)
|
||||
}
|
||||
for _, line := range historyLines {
|
||||
if err := rl.SaveToHistory(line); err != nil {
|
||||
fatalf("cannot initialize query history: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
outputMode := outputModeJSONMultiline
|
||||
s := ""
|
||||
for {
|
||||
line, err := rl.ReadLine()
|
||||
if err != nil {
|
||||
switch err {
|
||||
case io.EOF:
|
||||
if s != "" {
|
||||
// This is non-interactive query execution.
|
||||
executeQuery(context.Background(), rl, s, outputMode)
|
||||
}
|
||||
return
|
||||
case readline.ErrInterrupt:
|
||||
if s == "" && *incompleteLine == "" {
|
||||
fmt.Fprintf(rl, "interrupted\n")
|
||||
os.Exit(128 + int(syscall.SIGINT))
|
||||
}
|
||||
// Default value for Ctrl+C - clear the prompt and store the incompletely entered line into history
|
||||
s += *incompleteLine
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
rl.SetPrompt(firstLinePrompt)
|
||||
continue
|
||||
default:
|
||||
fatalf("unexpected error in readline: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
s += line
|
||||
if s == "" {
|
||||
// Skip empty lines
|
||||
continue
|
||||
}
|
||||
|
||||
if isQuitCommand(s) {
|
||||
fmt.Fprintf(rl, "bye!\n")
|
||||
_ = pushToHistory(rl, historyLines, s)
|
||||
return
|
||||
}
|
||||
if isHelpCommand(s) {
|
||||
printCommandsHelp(rl)
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
continue
|
||||
}
|
||||
if s == `\s` {
|
||||
fmt.Fprintf(rl, "singleline json output mode\n")
|
||||
outputMode = outputModeJSONSingleline
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
continue
|
||||
}
|
||||
if s == `\m` {
|
||||
fmt.Fprintf(rl, "multiline json output mode\n")
|
||||
outputMode = outputModeJSONMultiline
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
continue
|
||||
}
|
||||
if s == `\c` {
|
||||
fmt.Fprintf(rl, "compact output mode\n")
|
||||
outputMode = outputModeCompact
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
continue
|
||||
}
|
||||
if s == `\logfmt` {
|
||||
fmt.Fprintf(rl, "logfmt output mode\n")
|
||||
outputMode = outputModeLogfmt
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
continue
|
||||
}
|
||||
if line != "" && !strings.HasSuffix(line, ";") {
|
||||
// Assume the query is incomplete and allow the user finishing the query on the next line
|
||||
s += "\n"
|
||||
rl.SetPrompt(nextLinePrompt)
|
||||
continue
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
executeQuery(ctx, rl, s, outputMode)
|
||||
cancel()
|
||||
|
||||
historyLines = pushToHistory(rl, historyLines, s)
|
||||
s = ""
|
||||
rl.SetPrompt(firstLinePrompt)
|
||||
}
|
||||
}
|
||||
|
||||
func pushToHistory(rl *readline.Instance, historyLines []string, s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(historyLines) == 0 || historyLines[len(historyLines)-1] != s {
|
||||
historyLines = append(historyLines, s)
|
||||
if len(historyLines) > 500 {
|
||||
historyLines = historyLines[len(historyLines)-500:]
|
||||
}
|
||||
if err := saveToHistory(*historyFile, historyLines); err != nil {
|
||||
fatalf("cannot save query history: %s", err)
|
||||
}
|
||||
}
|
||||
if err := rl.SaveToHistory(s); err != nil {
|
||||
fatalf("cannot update query history: %s", err)
|
||||
}
|
||||
return historyLines
|
||||
}
|
||||
|
||||
func loadFromHistory(filePath string) ([]string, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
linesQuoted := strings.Split(string(data), "\n")
|
||||
lines := make([]string, 0, len(linesQuoted))
|
||||
i := 0
|
||||
for _, lineQuoted := range linesQuoted {
|
||||
i++
|
||||
if lineQuoted == "" {
|
||||
continue
|
||||
}
|
||||
line, err := strconv.Unquote(lineQuoted)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse line #%d at %s: %w; line: [%s]", i, filePath, err, line)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func saveToHistory(filePath string, lines []string) error {
|
||||
linesQuoted := make([]string, len(lines))
|
||||
for i, line := range lines {
|
||||
lineQuoted := strconv.Quote(line)
|
||||
linesQuoted[i] = lineQuoted
|
||||
}
|
||||
data := strings.Join(linesQuoted, "\n")
|
||||
return os.WriteFile(filePath, []byte(data), 0600)
|
||||
}
|
||||
|
||||
func isQuitCommand(s string) bool {
|
||||
switch s {
|
||||
case `\q`, "q", "quit", "exit":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isHelpCommand(s string) bool {
|
||||
switch s {
|
||||
case `\h`, "h", "help", "?":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func printCommandsHelp(w io.Writer) {
|
||||
fmt.Fprintf(w, "%s", `List of available commands:
|
||||
\q - quit
|
||||
\h - show this help
|
||||
\s - singleline json output mode
|
||||
\m - multiline json output mode
|
||||
\c - compact output
|
||||
\logfmt - logfmt output mode
|
||||
\tail <query> - live tail <query> results
|
||||
`)
|
||||
}
|
||||
|
||||
func executeQuery(ctx context.Context, output io.Writer, qStr string, outputMode outputMode) {
|
||||
if strings.HasPrefix(qStr, `\tail `) {
|
||||
tailQuery(ctx, output, qStr, outputMode)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := getQueryResponse(ctx, output, qStr, outputMode, *datasourceURL)
|
||||
if respBody == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = respBody.Close()
|
||||
}()
|
||||
|
||||
if err := readWithLess(respBody); err != nil {
|
||||
fmt.Fprintf(output, "error when reading query response: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func tailQuery(ctx context.Context, output io.Writer, qStr string, outputMode outputMode) {
|
||||
qStr = strings.TrimPrefix(qStr, `\tail `)
|
||||
qURL, err := getTailURL()
|
||||
if err != nil {
|
||||
fmt.Fprintf(output, "%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := getQueryResponse(ctx, output, qStr, outputMode, qURL)
|
||||
if respBody == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = respBody.Close()
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(output, respBody); err != nil {
|
||||
if !errors.Is(err, context.Canceled) && !isErrPipe(err) {
|
||||
fmt.Fprintf(output, "error when live tailing query response: %s\n", err)
|
||||
}
|
||||
fmt.Fprintf(output, "\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getTailURL() (string, error) {
|
||||
if *tailURL != "" {
|
||||
return *tailURL, nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(*datasourceURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot parse -datasource.url=%q: %w", *datasourceURL, err)
|
||||
}
|
||||
if !strings.HasSuffix(u.Path, "/query") {
|
||||
return "", fmt.Errorf("cannot find /query suffix in -datasource.url=%q", *datasourceURL)
|
||||
}
|
||||
u.Path = u.Path[:len(u.Path)-len("/query")] + "/tail"
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func getQueryResponse(ctx context.Context, output io.Writer, qStr string, outputMode outputMode, qURL string) io.ReadCloser {
|
||||
// Parse the query and convert it to canonical view.
|
||||
qStr = strings.TrimSuffix(qStr, ";")
|
||||
q, err := logstorage.ParseQuery(qStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(output, "cannot parse query: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
qStr = q.String()
|
||||
fmt.Fprintf(output, "executing [%s]...", qStr)
|
||||
|
||||
// Prepare HTTP request for qURL
|
||||
args := make(url.Values)
|
||||
args.Set("query", qStr)
|
||||
data := strings.NewReader(args.Encode())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", qURL, data)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("BUG: cannot prepare request to server: %w", err))
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
for _, h := range headers {
|
||||
req.Header.Set(h.Name, h.Value)
|
||||
}
|
||||
req.Header.Set("AccountID", strconv.Itoa(*accountID))
|
||||
req.Header.Set("ProjectID", strconv.Itoa(*projectID))
|
||||
|
||||
// Execute HTTP request at qURL
|
||||
startTime := time.Now()
|
||||
resp, err := httpClient.Do(req)
|
||||
queryDuration := time.Since(startTime)
|
||||
fmt.Fprintf(output, "; duration: %.3fs\n", queryDuration.Seconds())
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
fmt.Fprintf(output, "\n")
|
||||
} else {
|
||||
fmt.Fprintf(output, "cannot execute query: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify response code
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
body = []byte(fmt.Sprintf("cannot read response body: %s", err))
|
||||
}
|
||||
fmt.Fprintf(output, "unexpected status code: %d; response body:\n%s\n", resp.StatusCode, body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prettify the response body
|
||||
jp := newJSONPrettifier(resp.Body, outputMode)
|
||||
|
||||
return jp
|
||||
}
|
||||
|
||||
var httpClient = &http.Client{}
|
||||
|
||||
var headers []headerEntry
|
||||
|
||||
type headerEntry struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func parseHeaders(a []string) ([]headerEntry, error) {
|
||||
hes := make([]headerEntry, len(a))
|
||||
for i, s := range a {
|
||||
a := strings.SplitN(s, ":", 2)
|
||||
if len(a) != 2 {
|
||||
return nil, fmt.Errorf("cannot parse header=%q; it must contain at least one ':'; for example, 'Cookie: foo'", s)
|
||||
}
|
||||
hes[i] = headerEntry{
|
||||
Name: strings.TrimSpace(a[0]),
|
||||
Value: strings.TrimSpace(a[1]),
|
||||
}
|
||||
}
|
||||
return hes, nil
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
const s = `
|
||||
vlogscli is a command-line tool for querying VictoriaLogs.
|
||||
|
||||
See the docs at https://docs.victoriametrics.com/victorialogs/querying/vlogscli/
|
||||
`
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
11
app/vlogscli/multiarch/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
# See https://medium.com/on-docker/use-multi-stage-builds-to-inject-ca-certs-ad1e8f01de1b
|
||||
ARG certs_image=non-existing
|
||||
ARG root_image=non-existing
|
||||
FROM $certs_image AS certs
|
||||
RUN apk update && apk upgrade && apk --update --no-cache add ca-certificates
|
||||
|
||||
FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
ENTRYPOINT ["/vlogscli-prod"]
|
||||
ARG TARGETARCH
|
||||
COPY vlogscli-linux-${TARGETARCH}-prod ./vlogscli-prod
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -44,6 +45,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
if step <= 0 {
|
||||
httpserver.Errorf(w, r, "'step' must be bigger than zero")
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain offset
|
||||
@@ -380,6 +382,8 @@ func ProcessStreamsRequest(ctx context.Context, w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
// ProcessLiveTailRequest processes live tailing request to /select/logsq/tail
|
||||
//
|
||||
// See https://docs.victoriametrics.com/victorialogs/querying/#live-tailing
|
||||
func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
liveTailRequests.Inc()
|
||||
defer liveTailRequests.Dec()
|
||||
@@ -390,7 +394,9 @@ func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
if !q.CanLiveTail() {
|
||||
httpserver.Errorf(w, r, "the query [%s] cannot be used in live tailing; see https://docs.victoriametrics.com/victorialogs/querying/#live-tailing for details", q)
|
||||
httpserver.Errorf(w, r, "the query [%s] cannot be used in live tailing; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/querying/#live-tailing for details", q)
|
||||
return
|
||||
}
|
||||
q.Optimize()
|
||||
|
||||
@@ -413,13 +419,17 @@ func ProcessLiveTailRequest(ctx context.Context, w http.ResponseWriter, r *http.
|
||||
if !ok {
|
||||
logger.Panicf("BUG: it is expected that http.ResponseWriter (%T) supports http.Flusher interface", w)
|
||||
}
|
||||
qOrig := q
|
||||
for {
|
||||
start := end - tailOffsetNsecs
|
||||
end = time.Now().UnixNano()
|
||||
|
||||
qCopy := q.Clone()
|
||||
qCopy.AddTimeFilter(start, end)
|
||||
if err := vlstorage.RunQuery(ctxWithCancel, tenantIDs, qCopy, tp.writeBlock); err != nil {
|
||||
q = qOrig.Clone(end)
|
||||
q.AddTimeFilter(start, end)
|
||||
// q.Optimize() call is needed for converting '*' into filterNoop.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6785#issuecomment-2358547733
|
||||
q.Optimize()
|
||||
if err := vlstorage.RunQuery(ctxWithCancel, tenantIDs, q, tp.writeBlock); err != nil {
|
||||
httpserver.Errorf(w, r, "cannot execute tail query [%s]: %s", q, err)
|
||||
return
|
||||
}
|
||||
@@ -560,9 +570,212 @@ func (tp *tailProcessor) getTailRows() ([][]logstorage.Field, error) {
|
||||
return tailRows, nil
|
||||
}
|
||||
|
||||
// ProcessStatsQueryRangeRequest handles /select/logsql/stats_query_range request.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-log-range-stats
|
||||
func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
q, tenantIDs, err := parseCommonArgs(r)
|
||||
if err != nil {
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain step
|
||||
stepStr := r.FormValue("step")
|
||||
if stepStr == "" {
|
||||
stepStr = "1d"
|
||||
}
|
||||
step, err := promutils.ParseDuration(stepStr)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot parse 'step' arg: %s", err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
if step <= 0 {
|
||||
err := fmt.Errorf("'step' must be bigger than zero")
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain `by(...)` fields from the last `| stats` pipe in q.
|
||||
// Add `_time:step` to the `by(...)` list.
|
||||
byFields, err := q.GetStatsByFieldsAddGroupingByTime(int64(step))
|
||||
if err != nil {
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
q.Optimize()
|
||||
|
||||
m := make(map[string]*statsSeries)
|
||||
var mLock sync.Mutex
|
||||
|
||||
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
for i := range timestamps {
|
||||
timestamp := q.GetTimestamp()
|
||||
labels := make([]logstorage.Field, 0, len(byFields))
|
||||
for j, c := range columns {
|
||||
if c.Name == "_time" {
|
||||
nsec, ok := logstorage.TryParseTimestampRFC3339Nano(c.Values[i])
|
||||
if ok {
|
||||
timestamp = nsec
|
||||
continue
|
||||
}
|
||||
}
|
||||
if slices.Contains(byFields, c.Name) {
|
||||
labels = append(labels, logstorage.Field{
|
||||
Name: clonedColumnNames[j],
|
||||
Value: strings.Clone(c.Values[i]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var dst []byte
|
||||
for j, c := range columns {
|
||||
if !slices.Contains(byFields, c.Name) {
|
||||
name := clonedColumnNames[j]
|
||||
dst = dst[:0]
|
||||
dst = append(dst, name...)
|
||||
dst = logstorage.MarshalFieldsToJSON(dst, labels)
|
||||
key := string(dst)
|
||||
p := statsPoint{
|
||||
Timestamp: timestamp,
|
||||
Value: strings.Clone(c.Values[i]),
|
||||
}
|
||||
|
||||
mLock.Lock()
|
||||
ss := m[key]
|
||||
if ss == nil {
|
||||
ss = &statsSeries{
|
||||
key: key,
|
||||
Name: name,
|
||||
Labels: labels,
|
||||
}
|
||||
m[key] = ss
|
||||
}
|
||||
ss.Points = append(ss.Points, p)
|
||||
mLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := vlstorage.RunQuery(ctx, tenantIDs, q, writeBlock); err != nil {
|
||||
err = fmt.Errorf("cannot execute query [%s]: %s", q, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Sort the collected stats by time
|
||||
rows := make([]*statsSeries, 0, len(m))
|
||||
for _, ss := range m {
|
||||
points := ss.Points
|
||||
sort.Slice(points, func(i, j int) bool {
|
||||
return points[i].Timestamp < points[j].Timestamp
|
||||
})
|
||||
rows = append(rows, ss)
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].key < rows[j].key
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteStatsQueryRangeResponse(w, rows)
|
||||
}
|
||||
|
||||
type statsSeries struct {
|
||||
key string
|
||||
|
||||
Name string
|
||||
Labels []logstorage.Field
|
||||
Points []statsPoint
|
||||
}
|
||||
|
||||
type statsPoint struct {
|
||||
Timestamp int64
|
||||
Value string
|
||||
}
|
||||
|
||||
// ProcessStatsQueryRequest handles /select/logsql/stats_query request.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-log-stats
|
||||
func ProcessStatsQueryRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
q, tenantIDs, err := parseCommonArgs(r)
|
||||
if err != nil {
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Obtain `by(...)` fields from the last `| stats` pipe in q.
|
||||
byFields, err := q.GetStatsByFields()
|
||||
if err != nil {
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
q.Optimize()
|
||||
|
||||
var rows []statsRow
|
||||
var rowsLock sync.Mutex
|
||||
|
||||
timestamp := q.GetTimestamp()
|
||||
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
for i := range timestamps {
|
||||
labels := make([]logstorage.Field, 0, len(byFields))
|
||||
for j, c := range columns {
|
||||
if slices.Contains(byFields, c.Name) {
|
||||
labels = append(labels, logstorage.Field{
|
||||
Name: clonedColumnNames[j],
|
||||
Value: strings.Clone(c.Values[i]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for j, c := range columns {
|
||||
if !slices.Contains(byFields, c.Name) {
|
||||
r := statsRow{
|
||||
Name: clonedColumnNames[j],
|
||||
Labels: labels,
|
||||
Timestamp: timestamp,
|
||||
Value: strings.Clone(c.Values[i]),
|
||||
}
|
||||
|
||||
rowsLock.Lock()
|
||||
rows = append(rows, r)
|
||||
rowsLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := vlstorage.RunQuery(ctx, tenantIDs, q, writeBlock); err != nil {
|
||||
err = fmt.Errorf("cannot execute query [%s]: %s", q, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
WriteStatsQueryResponse(w, rows)
|
||||
}
|
||||
|
||||
type statsRow struct {
|
||||
Name string
|
||||
Labels []logstorage.Field
|
||||
Timestamp int64
|
||||
Value string
|
||||
}
|
||||
|
||||
// ProcessQueryRequest handles /select/logsql/query request.
|
||||
//
|
||||
// See https://docs.victoriametrics.com/victorialogs/querying/#http-api
|
||||
// See https://docs.victoriametrics.com/victorialogs/querying/#querying-logs
|
||||
func ProcessQueryRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
q, tenantIDs, err := parseCommonArgs(r)
|
||||
if err != nil {
|
||||
@@ -637,6 +850,7 @@ func getLastNQueryResults(ctx context.Context, tenantIDs []logstorage.TenantID,
|
||||
limitUpper := 2 * limit
|
||||
q.AddPipeLimit(uint64(limitUpper))
|
||||
q.Optimize()
|
||||
|
||||
rows, err := getQueryResultsWithLimit(ctx, tenantIDs, q, limitUpper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -647,32 +861,62 @@ func getLastNQueryResults(ctx context.Context, tenantIDs []logstorage.TenantID,
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Slow path - search for the time range containing up to limitUpper rows.
|
||||
// Slow path - adjust time range for selecting up to limitUpper rows
|
||||
start, end := q.GetFilterTimeRange()
|
||||
d := end/2 - start/2
|
||||
start += d
|
||||
|
||||
qOrig := q
|
||||
for {
|
||||
q = qOrig.Clone()
|
||||
timestamp := qOrig.GetTimestamp()
|
||||
q = qOrig.Clone(timestamp)
|
||||
q.AddTimeFilter(start, end)
|
||||
// q.Optimize() call is needed for converting '*' into filterNoop.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6785#issuecomment-2358547733
|
||||
q.Optimize()
|
||||
rows, err := getQueryResultsWithLimit(ctx, tenantIDs, q, limitUpper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(rows) >= limit && len(rows) < limitUpper || d == 0 {
|
||||
if d == 0 || start >= end {
|
||||
// The [start ... end] time range equals one nanosecond.
|
||||
// Just return up to limit rows.
|
||||
if len(rows) > limit {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
dLastBit := d & 1
|
||||
d /= 2
|
||||
|
||||
if len(rows) >= limitUpper {
|
||||
// The number of found rows on the [start ... end] time range exceeds limitUpper,
|
||||
// so reduce the time range to [start+d ... end].
|
||||
start += d
|
||||
continue
|
||||
}
|
||||
if len(rows) >= limit {
|
||||
// The number of found rows is in the range [limit ... limitUpper).
|
||||
// This means that found rows contains the needed limit rows with the biggest timestamps.
|
||||
rows = getLastNRows(rows, limit)
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
lastBit := d & 1
|
||||
d /= 2
|
||||
if len(rows) > limit {
|
||||
start += d
|
||||
} else {
|
||||
start -= d + lastBit
|
||||
// The number of found rows on [start ... end] time range is below the limit.
|
||||
// This means the time range doesn't cover the needed logs, so it must be extended.
|
||||
|
||||
if len(rows) == 0 {
|
||||
// The [start ... end] time range doesn't contain any rows, so change it to [start-d ... start).
|
||||
end = start - 1
|
||||
start -= d + dLastBit
|
||||
continue
|
||||
}
|
||||
|
||||
// The number of found rows on [start ... end] time range is bigger than 0 but smaller than limit.
|
||||
// Increase the time range to [start-d ... end].
|
||||
start -= d + dLastBit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,20 +937,25 @@ func getQueryResultsWithLimit(ctx context.Context, tenantIDs []logstorage.Tenant
|
||||
var rows []row
|
||||
var rowsLock sync.Mutex
|
||||
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
|
||||
rowsLock.Lock()
|
||||
defer rowsLock.Unlock()
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
|
||||
for i, timestamp := range timestamps {
|
||||
fields := make([]logstorage.Field, len(columns))
|
||||
for j := range columns {
|
||||
f := &fields[j]
|
||||
f.Name = strings.Clone(columns[j].Name)
|
||||
f.Name = clonedColumnNames[j]
|
||||
f.Value = strings.Clone(columns[j].Values[i])
|
||||
}
|
||||
|
||||
rowsLock.Lock()
|
||||
rows = append(rows, row{
|
||||
timestamp: timestamp,
|
||||
fields: fields,
|
||||
})
|
||||
rowsLock.Unlock()
|
||||
}
|
||||
|
||||
if len(rows) >= limit {
|
||||
@@ -728,9 +977,23 @@ func parseCommonArgs(r *http.Request) (*logstorage.Query, []logstorage.TenantID,
|
||||
}
|
||||
tenantIDs := []logstorage.TenantID{tenantID}
|
||||
|
||||
// Parse optional time arg
|
||||
timestamp, okTime, err := getTimeNsec(r, "time")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !okTime {
|
||||
// If time arg is missing, then evaluate query at the current timestamp
|
||||
timestamp = time.Now().UnixNano()
|
||||
}
|
||||
|
||||
// decrease timestamp by one nanosecond in order to avoid capturing logs belonging
|
||||
// to the first nanosecond at the next period of time (month, week, day, hour, etc.)
|
||||
timestamp--
|
||||
|
||||
// Parse query
|
||||
qStr := r.FormValue("query")
|
||||
q, err := logstorage.ParseQuery(qStr)
|
||||
q, err := logstorage.ParseQueryAtTimestamp(qStr, timestamp)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot parse query [%s]: %s", qStr, err)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,31 @@
|
||||
|
||||
// JSONRow creates JSON row from the given fields.
|
||||
{% func JSONRow(columns []logstorage.BlockColumn, rowIdx int) %}
|
||||
{
|
||||
{% code c := &columns[0] %}
|
||||
{% code
|
||||
i := 0
|
||||
for i < len(columns) && columns[i].Values[rowIdx] == "" {
|
||||
i++
|
||||
}
|
||||
columns = columns[i:]
|
||||
%}
|
||||
{% if len(columns) == 0 %}
|
||||
{% return %}
|
||||
{% endif %}
|
||||
{
|
||||
{% code c := &columns[0] %}
|
||||
{%q= c.Name %}:{%q= c.Values[rowIdx] %}
|
||||
{% code columns = columns[1:] %}
|
||||
{% for colIdx := range columns %}
|
||||
{% code c := &columns[colIdx] %}
|
||||
{% code
|
||||
c := &columns[colIdx]
|
||||
v := c.Values[rowIdx]
|
||||
%}
|
||||
{% if v == "" %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
,{%q= c.Name %}:{%q= c.Values[rowIdx] %}
|
||||
{% endfor %}
|
||||
}{% newline %}
|
||||
}{% newline %}
|
||||
{% endfunc %}
|
||||
|
||||
// JSONRows prints formatted rows
|
||||
@@ -23,7 +39,11 @@
|
||||
{% return %}
|
||||
{% endif %}
|
||||
{% for _, fields := range rows %}
|
||||
{
|
||||
{% code fields = logstorage.SkipLeadingFieldsWithoutValues(fields) %}
|
||||
{% if len(fields) == 0 %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
{
|
||||
{% if len(fields) > 0 %}
|
||||
{% code
|
||||
f := fields[0]
|
||||
@@ -31,10 +51,13 @@
|
||||
%}
|
||||
{%q= f.Name %}:{%q= f.Value %}
|
||||
{% for _, f := range fields %}
|
||||
{% if f.Value == "" %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
,{%q= f.Name %}:{%q= f.Value %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
}{% newline %}
|
||||
}{% newline %}
|
||||
{% endfor %}
|
||||
{% endfunc %}
|
||||
|
||||
|
||||
@@ -26,141 +26,176 @@ var (
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:8
|
||||
func StreamJSONRow(qw422016 *qt422016.Writer, columns []logstorage.BlockColumn, rowIdx int) {
|
||||
//line app/vlselect/logsql/query_response.qtpl:8
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:10
|
||||
i := 0
|
||||
for i < len(columns) && columns[i].Values[rowIdx] == "" {
|
||||
i++
|
||||
}
|
||||
columns = columns[i:]
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:16
|
||||
if len(columns) == 0 {
|
||||
//line app/vlselect/logsql/query_response.qtpl:17
|
||||
return
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:20
|
||||
c := &columns[0]
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:11
|
||||
//line app/vlselect/logsql/query_response.qtpl:21
|
||||
qw422016.N().Q(c.Name)
|
||||
//line app/vlselect/logsql/query_response.qtpl:11
|
||||
//line app/vlselect/logsql/query_response.qtpl:21
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:11
|
||||
//line app/vlselect/logsql/query_response.qtpl:21
|
||||
qw422016.N().Q(c.Values[rowIdx])
|
||||
//line app/vlselect/logsql/query_response.qtpl:12
|
||||
//line app/vlselect/logsql/query_response.qtpl:22
|
||||
columns = columns[1:]
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:13
|
||||
//line app/vlselect/logsql/query_response.qtpl:23
|
||||
for colIdx := range columns {
|
||||
//line app/vlselect/logsql/query_response.qtpl:14
|
||||
//line app/vlselect/logsql/query_response.qtpl:25
|
||||
c := &columns[colIdx]
|
||||
v := c.Values[rowIdx]
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:14
|
||||
//line app/vlselect/logsql/query_response.qtpl:28
|
||||
if v == "" {
|
||||
//line app/vlselect/logsql/query_response.qtpl:29
|
||||
continue
|
||||
//line app/vlselect/logsql/query_response.qtpl:30
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:30
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:15
|
||||
//line app/vlselect/logsql/query_response.qtpl:31
|
||||
qw422016.N().Q(c.Name)
|
||||
//line app/vlselect/logsql/query_response.qtpl:15
|
||||
//line app/vlselect/logsql/query_response.qtpl:31
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:15
|
||||
//line app/vlselect/logsql/query_response.qtpl:31
|
||||
qw422016.N().Q(c.Values[rowIdx])
|
||||
//line app/vlselect/logsql/query_response.qtpl:16
|
||||
//line app/vlselect/logsql/query_response.qtpl:32
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:16
|
||||
//line app/vlselect/logsql/query_response.qtpl:32
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:17
|
||||
//line app/vlselect/logsql/query_response.qtpl:33
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
func WriteJSONRow(qq422016 qtio422016.Writer, columns []logstorage.BlockColumn, rowIdx int) {
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
StreamJSONRow(qw422016, columns, rowIdx)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
func JSONRow(columns []logstorage.BlockColumn, rowIdx int) string {
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
WriteJSONRow(qb422016, columns, rowIdx)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/query_response.qtpl:18
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
}
|
||||
|
||||
// JSONRows prints formatted rows
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:21
|
||||
//line app/vlselect/logsql/query_response.qtpl:37
|
||||
func StreamJSONRows(qw422016 *qt422016.Writer, rows [][]logstorage.Field) {
|
||||
//line app/vlselect/logsql/query_response.qtpl:22
|
||||
//line app/vlselect/logsql/query_response.qtpl:38
|
||||
if len(rows) == 0 {
|
||||
//line app/vlselect/logsql/query_response.qtpl:23
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
return
|
||||
//line app/vlselect/logsql/query_response.qtpl:24
|
||||
//line app/vlselect/logsql/query_response.qtpl:40
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:25
|
||||
//line app/vlselect/logsql/query_response.qtpl:41
|
||||
for _, fields := range rows {
|
||||
//line app/vlselect/logsql/query_response.qtpl:25
|
||||
//line app/vlselect/logsql/query_response.qtpl:42
|
||||
fields = logstorage.SkipLeadingFieldsWithoutValues(fields)
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:43
|
||||
if len(fields) == 0 {
|
||||
//line app/vlselect/logsql/query_response.qtpl:44
|
||||
continue
|
||||
//line app/vlselect/logsql/query_response.qtpl:45
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:45
|
||||
qw422016.N().S(`{`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:27
|
||||
//line app/vlselect/logsql/query_response.qtpl:47
|
||||
if len(fields) > 0 {
|
||||
//line app/vlselect/logsql/query_response.qtpl:29
|
||||
//line app/vlselect/logsql/query_response.qtpl:49
|
||||
f := fields[0]
|
||||
fields = fields[1:]
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:32
|
||||
//line app/vlselect/logsql/query_response.qtpl:52
|
||||
qw422016.N().Q(f.Name)
|
||||
//line app/vlselect/logsql/query_response.qtpl:32
|
||||
//line app/vlselect/logsql/query_response.qtpl:52
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:32
|
||||
//line app/vlselect/logsql/query_response.qtpl:52
|
||||
qw422016.N().Q(f.Value)
|
||||
//line app/vlselect/logsql/query_response.qtpl:33
|
||||
//line app/vlselect/logsql/query_response.qtpl:53
|
||||
for _, f := range fields {
|
||||
//line app/vlselect/logsql/query_response.qtpl:33
|
||||
//line app/vlselect/logsql/query_response.qtpl:54
|
||||
if f.Value == "" {
|
||||
//line app/vlselect/logsql/query_response.qtpl:55
|
||||
continue
|
||||
//line app/vlselect/logsql/query_response.qtpl:56
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:56
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
//line app/vlselect/logsql/query_response.qtpl:57
|
||||
qw422016.N().Q(f.Name)
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
//line app/vlselect/logsql/query_response.qtpl:57
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:34
|
||||
//line app/vlselect/logsql/query_response.qtpl:57
|
||||
qw422016.N().Q(f.Value)
|
||||
//line app/vlselect/logsql/query_response.qtpl:35
|
||||
//line app/vlselect/logsql/query_response.qtpl:58
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:36
|
||||
//line app/vlselect/logsql/query_response.qtpl:59
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:36
|
||||
//line app/vlselect/logsql/query_response.qtpl:59
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:37
|
||||
//line app/vlselect/logsql/query_response.qtpl:60
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line app/vlselect/logsql/query_response.qtpl:38
|
||||
//line app/vlselect/logsql/query_response.qtpl:61
|
||||
}
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
func WriteJSONRows(qq422016 qtio422016.Writer, rows [][]logstorage.Field) {
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
StreamJSONRows(qw422016, rows)
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
func JSONRows(rows [][]logstorage.Field) string {
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
WriteJSONRows(qb422016, rows)
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/query_response.qtpl:39
|
||||
//line app/vlselect/logsql/query_response.qtpl:62
|
||||
}
|
||||
|
||||
52
app/vlselect/logsql/stats_query_range_response.qtpl
Normal file
@@ -0,0 +1,52 @@
|
||||
{% stripspace %}
|
||||
|
||||
// StatsQueryRangeResponse generates response for /select/logsql/stats_query_range
|
||||
{% func StatsQueryRangeResponse(rows []*statsSeries) %}
|
||||
{
|
||||
"status":"success",
|
||||
"data":{
|
||||
"resultType":"matrix",
|
||||
"result":[
|
||||
{% if len(rows) > 0 %}
|
||||
{%= formatStatsSeries(rows[0]) %}
|
||||
{% code rows = rows[1:] %}
|
||||
{% for i := range rows %}
|
||||
,{%= formatStatsSeries(rows[i]) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
]
|
||||
}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func formatStatsSeries(ss *statsSeries) %}
|
||||
{
|
||||
"metric":{
|
||||
"__name__":{%q= ss.Name %}
|
||||
{% if len(ss.Labels) > 0 %}
|
||||
{% for _, label := range ss.Labels %}
|
||||
,{%q= label.Name %}:{%q= label.Value %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
},
|
||||
"values":[
|
||||
{% code points := ss.Points %}
|
||||
{% if len(points) > 0 %}
|
||||
{%= formatStatsPoint(&points[0]) %}
|
||||
{% code points = points[1:] %}
|
||||
{% for i := range points %}
|
||||
,{%= formatStatsPoint(&points[i]) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
]
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func formatStatsPoint(p *statsPoint) %}
|
||||
[
|
||||
{%f= float64(p.Timestamp)/1e9 %},
|
||||
{%q= p.Value %}
|
||||
]
|
||||
{% endfunc %}
|
||||
|
||||
{% endstripspace %}
|
||||
188
app/vlselect/logsql/stats_query_range_response.qtpl.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Code generated by qtc from "stats_query_range_response.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
// StatsQueryRangeResponse generates response for /select/logsql/stats_query_range
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||
package logsql
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||
func StreamStatsQueryRangeResponse(qw422016 *qt422016.Writer, rows []*statsSeries) {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:4
|
||||
qw422016.N().S(`{"status":"success","data":{"resultType":"matrix","result":[`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:10
|
||||
if len(rows) > 0 {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:11
|
||||
streamformatStatsSeries(qw422016, rows[0])
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:12
|
||||
rows = rows[1:]
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:13
|
||||
for i := range rows {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:13
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:14
|
||||
streamformatStatsSeries(qw422016, rows[i])
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:15
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:16
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:16
|
||||
qw422016.N().S(`]}}`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
func WriteStatsQueryRangeResponse(qq422016 qtio422016.Writer, rows []*statsSeries) {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
StreamStatsQueryRangeResponse(qw422016, rows)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
func StatsQueryRangeResponse(rows []*statsSeries) string {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
WriteStatsQueryRangeResponse(qb422016, rows)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:20
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:22
|
||||
func streamformatStatsSeries(qw422016 *qt422016.Writer, ss *statsSeries) {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:22
|
||||
qw422016.N().S(`{"metric":{"__name__":`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:25
|
||||
qw422016.N().Q(ss.Name)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:26
|
||||
if len(ss.Labels) > 0 {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:27
|
||||
for _, label := range ss.Labels {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:27
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:28
|
||||
qw422016.N().Q(label.Name)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:28
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:28
|
||||
qw422016.N().Q(label.Value)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:29
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:30
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:30
|
||||
qw422016.N().S(`},"values":[`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:33
|
||||
points := ss.Points
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:34
|
||||
if len(points) > 0 {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:35
|
||||
streamformatStatsPoint(qw422016, &points[0])
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:36
|
||||
points = points[1:]
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:37
|
||||
for i := range points {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:37
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:38
|
||||
streamformatStatsPoint(qw422016, &points[i])
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:39
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:40
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:40
|
||||
qw422016.N().S(`]}`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
func writeformatStatsSeries(qq422016 qtio422016.Writer, ss *statsSeries) {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
streamformatStatsSeries(qw422016, ss)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
func formatStatsSeries(ss *statsSeries) string {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
writeformatStatsSeries(qb422016, ss)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:43
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:45
|
||||
func streamformatStatsPoint(qw422016 *qt422016.Writer, p *statsPoint) {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:45
|
||||
qw422016.N().S(`[`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:47
|
||||
qw422016.N().F(float64(p.Timestamp) / 1e9)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:47
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:48
|
||||
qw422016.N().Q(p.Value)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:48
|
||||
qw422016.N().S(`]`)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
func writeformatStatsPoint(qq422016 qtio422016.Writer, p *statsPoint) {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
streamformatStatsPoint(qw422016, p)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
func formatStatsPoint(p *statsPoint) string {
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
writeformatStatsPoint(qb422016, p)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/stats_query_range_response.qtpl:50
|
||||
}
|
||||
36
app/vlselect/logsql/stats_query_response.qtpl
Normal file
@@ -0,0 +1,36 @@
|
||||
{% stripspace %}
|
||||
|
||||
// StatsQueryResponse generates response for /select/logsql/stats_query
|
||||
{% func StatsQueryResponse(rows []statsRow) %}
|
||||
{
|
||||
"status":"success",
|
||||
"data":{
|
||||
"resultType":"vector",
|
||||
"result":[
|
||||
{% if len(rows) > 0 %}
|
||||
{%= formatStatsRow(&rows[0]) %}
|
||||
{% code rows = rows[1:] %}
|
||||
{% for i := range rows %}
|
||||
,{%= formatStatsRow(&rows[i]) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
]
|
||||
}
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% func formatStatsRow(r *statsRow) %}
|
||||
{
|
||||
"metric":{
|
||||
"__name__":{%q= r.Name %}
|
||||
{% if len(r.Labels) > 0 %}
|
||||
{% for _, label := range r.Labels %}
|
||||
,{%q= label.Name %}:{%q= label.Value %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
},
|
||||
"value":[{%f= float64(r.Timestamp)/1e9 %},{%q= r.Value %}]
|
||||
}
|
||||
{% endfunc %}
|
||||
|
||||
{% endstripspace %}
|
||||
133
app/vlselect/logsql/stats_query_response.qtpl.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Code generated by qtc from "stats_query_response.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
// StatsQueryResponse generates response for /select/logsql/stats_query
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:4
|
||||
package logsql
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:4
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:4
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:4
|
||||
func StreamStatsQueryResponse(qw422016 *qt422016.Writer, rows []statsRow) {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:4
|
||||
qw422016.N().S(`{"status":"success","data":{"resultType":"vector","result":[`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:10
|
||||
if len(rows) > 0 {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:11
|
||||
streamformatStatsRow(qw422016, &rows[0])
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:12
|
||||
rows = rows[1:]
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:13
|
||||
for i := range rows {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:13
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:14
|
||||
streamformatStatsRow(qw422016, &rows[i])
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:15
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:16
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:16
|
||||
qw422016.N().S(`]}}`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
func WriteStatsQueryResponse(qq422016 qtio422016.Writer, rows []statsRow) {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
StreamStatsQueryResponse(qw422016, rows)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
func StatsQueryResponse(rows []statsRow) string {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
WriteStatsQueryResponse(qb422016, rows)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:20
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:22
|
||||
func streamformatStatsRow(qw422016 *qt422016.Writer, r *statsRow) {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:22
|
||||
qw422016.N().S(`{"metric":{"__name__":`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:25
|
||||
qw422016.N().Q(r.Name)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:26
|
||||
if len(r.Labels) > 0 {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:27
|
||||
for _, label := range r.Labels {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:27
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:28
|
||||
qw422016.N().Q(label.Name)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:28
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:28
|
||||
qw422016.N().Q(label.Value)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:29
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:30
|
||||
}
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:30
|
||||
qw422016.N().S(`},"value":[`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:32
|
||||
qw422016.N().F(float64(r.Timestamp) / 1e9)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:32
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:32
|
||||
qw422016.N().Q(r.Value)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:32
|
||||
qw422016.N().S(`]}`)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
func writeformatStatsRow(qq422016 qtio422016.Writer, r *statsRow) {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
streamformatStatsRow(qw422016, r)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
}
|
||||
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
func formatStatsRow(r *statsRow) string {
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
writeformatStatsRow(qb422016, r)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
return qs422016
|
||||
//line app/vlselect/logsql/stats_query_response.qtpl:34
|
||||
}
|
||||
@@ -193,6 +193,14 @@ func processSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
logsqlQueryRequests.Inc()
|
||||
logsql.ProcessQueryRequest(ctx, w, r)
|
||||
return true
|
||||
case "/select/logsql/stats_query":
|
||||
logsqlStatsQueryRequests.Inc()
|
||||
logsql.ProcessStatsQueryRequest(ctx, w, r)
|
||||
return true
|
||||
case "/select/logsql/stats_query_range":
|
||||
logsqlStatsQueryRangeRequests.Inc()
|
||||
logsql.ProcessStatsQueryRangeRequest(ctx, w, r)
|
||||
return true
|
||||
case "/select/logsql/stream_field_names":
|
||||
logsqlStreamFieldNamesRequests.Inc()
|
||||
logsql.ProcessStreamFieldNamesRequest(ctx, w, r)
|
||||
@@ -232,6 +240,8 @@ var (
|
||||
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
|
||||
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
|
||||
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
|
||||
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
|
||||
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
|
||||
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
|
||||
logsqlStreamFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_values"}`)
|
||||
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.c9cc37dd.css",
|
||||
"main.js": "./static/js/main.7ae2e2c4.js",
|
||||
"static/js/685.bebe1265.chunk.js": "./static/js/685.bebe1265.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.8c2e588d62b87f90dbf0.md",
|
||||
"main.css": "./static/css/main.faf86aa5.css",
|
||||
"main.js": "./static/js/main.2810cc52.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.c9cc37dd.css",
|
||||
"static/js/main.7ae2e2c4.js"
|
||||
"static/css/main.faf86aa5.css",
|
||||
"static/js/main.2810cc52.js"
|
||||
]
|
||||
}
|
||||
5
app/vlselect/vmui/config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"license": {
|
||||
"type": "opensource"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
1
app/vlselect/vmui/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0" fill="#020202"/><path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z" fill="#020202"/><path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z" fill="#020202"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.7ae2e2c4.js"></script><link href="./static/css/main.c9cc37dd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.2810cc52.js"></script><link href="./static/css/main.faf86aa5.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
@@ -1,16 +1,11 @@
|
||||
{
|
||||
"short_name": "Victoria Metrics UI",
|
||||
"name": "Victoria Metrics UI is a metric explorer for Victoria Metrics",
|
||||
"short_name": "vmui",
|
||||
"name": "vmui",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon-32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "apple-touch-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
"src": "favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 6.5 KiB |
1
app/vlselect/vmui/static/css/main.faf86aa5.css
Normal file
1
app/vlselect/vmui/static/js/685.f772060c.chunk.js
Normal file
2
app/vlselect/vmui/static/js/main.2810cc52.js
Normal file
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.17.0
|
||||
* @remix-run/router v1.19.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router DOM v6.24.0
|
||||
* React Router DOM v6.26.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@@ -27,7 +27,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.24.0
|
||||
* React Router v6.26.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
@@ -103,7 +103,7 @@ The list of MetricsQL features on top of PromQL:
|
||||
* Trailing commas on all the lists are allowed - label filters, function args and with expressions.
|
||||
For instance, the following queries are valid: `m{foo="bar",}`, `f(a, b,)`, `WITH (x=y,) x`.
|
||||
This simplifies maintenance of multi-line queries.
|
||||
* Metric names and label names may contain any unicode letter. For example `температура{город="Київ"}` is a valid MetricsQL expression.
|
||||
* Metric names and label names may contain any unicode letter. For example `ტემპერატურა{πόλη="Київ"}` is a valid MetricsQL expression.
|
||||
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
|
||||
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
|
||||
Additionally, the following escape sequences are supported:
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="UI for VictoriaMetrics"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png"/>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>VM UI</title>
|
||||
<script src="%PUBLIC_URL%/dashboards/index.js" type="module"></script>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta name="twitter:title" content="UI for VictoriaMetrics">
|
||||
<meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta name="twitter:site" content="@VictoriaMetrics">
|
||||
|
||||
<meta property="og:title" content="Metric explorer for VictoriaMetrics">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta property="og:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta property="og:type" content="website">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="UI for VictoriaMetrics"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png"/>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>VM UI</title>
|
||||
<script src="%PUBLIC_URL%/dashboards/index.js" type="module"></script>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta name="twitter:title" content="UI for VictoriaMetrics">
|
||||
<meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta name="twitter:site" content="@VictoriaMetrics">
|
||||
|
||||
<meta property="og:title" content="Metric explorer for VictoriaMetrics">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta property="og:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta property="og:type" content="website">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
retentionPeriod = flagutil.NewDuration("retentionPeriod", "7d", "Log entries with timestamps older than now-retentionPeriod are automatically deleted; "+
|
||||
retentionPeriod = flagutil.NewRetentionDuration("retentionPeriod", "7d", "Log entries with timestamps older than now-retentionPeriod are automatically deleted; "+
|
||||
"log entries with timestamps outside the retention are also rejected during data ingestion; the minimum supported retention is 1d (one day); "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/#retention ; see also -retention.maxDiskSpaceUsageBytes")
|
||||
maxDiskSpaceUsageBytes = flagutil.NewBytes("retention.maxDiskSpaceUsageBytes", 0, "The maximum disk space usage at -storageDataPath before older per-day "+
|
||||
"partitions are automatically dropped; see https://docs.victoriametrics.com/victorialogs/#retention-by-disk-space-usage ; see also -retentionPeriod")
|
||||
futureRetention = flagutil.NewDuration("futureRetention", "2d", "Log entries with timestamps bigger than now+futureRetention are rejected during data ingestion; "+
|
||||
futureRetention = flagutil.NewRetentionDuration("futureRetention", "2d", "Log entries with timestamps bigger than now+futureRetention are rejected during data ingestion; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/#retention")
|
||||
storageDataPath = flag.String("storageDataPath", "victoria-logs-data", "Path to directory where to store VictoriaLogs data; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/#storage")
|
||||
@@ -37,6 +37,8 @@ var (
|
||||
"see https://docs.victoriametrics.com/victorialogs/data-ingestion/ ; see also -logNewStreams")
|
||||
minFreeDiskSpaceBytes = flagutil.NewBytes("storage.minFreeDiskSpaceBytes", 10e6, "The minimum free disk space at -storageDataPath after which "+
|
||||
"the storage stops accepting new data")
|
||||
|
||||
forceMergeAuthKey = flagutil.NewPassword("forceMergeAuthKey", "authKey, which must be passed in query string to /internal/force_merge pages. It overrides -httpAuth.*")
|
||||
)
|
||||
|
||||
// Init initializes vlstorage.
|
||||
@@ -87,6 +89,28 @@ func Stop() {
|
||||
strg = nil
|
||||
}
|
||||
|
||||
// RequestHandler is a storage request handler.
|
||||
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
if path == "/internal/force_merge" {
|
||||
if !httpserver.CheckAuthFlag(w, r, forceMergeAuthKey) {
|
||||
return true
|
||||
}
|
||||
// Run force merge in background
|
||||
partitionNamePrefix := r.FormValue("partition_prefix")
|
||||
go func() {
|
||||
activeForceMerges.Inc()
|
||||
defer activeForceMerges.Dec()
|
||||
logger.Infof("forced merge for partition_prefix=%q has been started", partitionNamePrefix)
|
||||
startTime := time.Now()
|
||||
strg.MustForceMerge(partitionNamePrefix)
|
||||
logger.Infof("forced merge for partition_prefix=%q has been successfully finished in %.3f seconds", partitionNamePrefix, time.Since(startTime).Seconds())
|
||||
}()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var strg *logstorage.Storage
|
||||
var storageMetrics *metrics.Set
|
||||
|
||||
@@ -205,3 +229,5 @@ func writeStorageMetrics(w io.Writer, strg *logstorage.Storage) {
|
||||
metrics.WriteCounterUint64(w, `vl_rows_dropped_total{reason="too_big_timestamp"}`, ss.RowsDroppedTooBigTimestamp)
|
||||
metrics.WriteCounterUint64(w, `vl_rows_dropped_total{reason="too_small_timestamp"}`, ss.RowsDroppedTooSmallTimestamp)
|
||||
}
|
||||
|
||||
var activeForceMerges = metrics.NewCounter("vl_active_force_merges")
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
//
|
||||
// See https://github.com/influxdata/telegraf/tree/master/plugins/inputs/socket_listener/
|
||||
func InsertHandlerForReader(at *auth.Token, r io.Reader, isGzipped bool) error {
|
||||
return stream.Parse(r, isGzipped, "", "", func(db string, rows []parser.Row) error {
|
||||
return stream.Parse(r, true, isGzipped, "", "", func(db string, rows []parser.Row) error {
|
||||
return insertRows(at, db, rows, nil)
|
||||
})
|
||||
}
|
||||
@@ -50,11 +50,12 @@ func InsertHandlerForHTTP(at *auth.Token, req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
isGzipped := req.Header.Get("Content-Encoding") == "gzip"
|
||||
isStreamMode := req.Header.Get("Stream-Mode") == "1"
|
||||
q := req.URL.Query()
|
||||
precision := q.Get("precision")
|
||||
// Read db tag from https://docs.influxdata.com/influxdb/v1.7/tools/api/#write-http-endpoint
|
||||
db := q.Get("db")
|
||||
return stream.Parse(req.Body, isGzipped, precision, db, func(db string, rows []parser.Row) error {
|
||||
return stream.Parse(req.Body, isStreamMode, isGzipped, precision, db, func(db string, rows []parser.Row) error {
|
||||
return insertRows(at, db, rows, extraLabels)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 8429
|
||||
ENTRYPOINT ["/vmagent-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmagent-linux-${TARGETARCH}-prod ./vmagent-prod
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/awsapi"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
@@ -23,6 +22,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/ratelimiter"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -35,7 +35,7 @@ var (
|
||||
"By default, the rate limit is disabled. It can be useful for limiting load on remote storage when big amounts of buffered data "+
|
||||
"is sent after temporary unavailability of the remote storage. See also -maxIngestionRate")
|
||||
sendTimeout = flagutil.NewArrayDuration("remoteWrite.sendTimeout", time.Minute, "Timeout for sending a single block of data to the corresponding -remoteWrite.url")
|
||||
retryMinInterval = flagutil.NewArrayDuration("remoteWrite.retryMinInterval", time.Second, "The minimum delay between retry attempts to send a block of data to the corresponding -remoteWrite.url. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxInterval")
|
||||
retryMinInterval = flagutil.NewArrayDuration("remoteWrite.retryMinInterval", time.Second, "The minimum delay between retry attempts to send a block of data to the corresponding -remoteWrite.url. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxTime")
|
||||
retryMaxTime = flagutil.NewArrayDuration("remoteWrite.retryMaxTime", time.Minute, "The max time spent on retry attempts to send a block of data to the corresponding -remoteWrite.url. Change this value if it is expected for -remoteWrite.url to be unreachable for more than -remoteWrite.retryMaxTime. See also -remoteWrite.retryMinInterval")
|
||||
proxyURL = flagutil.NewArrayString("remoteWrite.proxyURL", "Optional proxy URL for writing data to the corresponding -remoteWrite.url. "+
|
||||
"Supported proxies: http, https, socks5. Example: -remoteWrite.proxyURL=socks5://proxy:1234")
|
||||
@@ -463,10 +463,10 @@ again:
|
||||
|
||||
// Unexpected status code returned
|
||||
retriesCount++
|
||||
retryDuration *= 2
|
||||
if retryDuration > maxRetryDuration {
|
||||
retryDuration = maxRetryDuration
|
||||
}
|
||||
retryAfterHeader := parseRetryAfterHeader(resp.Header.Get("Retry-After"))
|
||||
retryDuration = getRetryDuration(retryAfterHeader, retryDuration, maxRetryDuration)
|
||||
|
||||
// Handle response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
@@ -488,3 +488,49 @@ again:
|
||||
}
|
||||
|
||||
var remoteWriteRejectedLogger = logger.WithThrottler("remoteWriteRejected", 5*time.Second)
|
||||
|
||||
// getRetryDuration returns retry duration.
|
||||
// retryAfterDuration has the highest priority.
|
||||
// If retryAfterDuration is not specified, retryDuration gets doubled.
|
||||
// retryDuration can't exceed maxRetryDuration.
|
||||
//
|
||||
// Also see: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6097
|
||||
func getRetryDuration(retryAfterDuration, retryDuration, maxRetryDuration time.Duration) time.Duration {
|
||||
// retryAfterDuration has the highest priority duration
|
||||
if retryAfterDuration > 0 {
|
||||
return timeutil.AddJitterToDuration(retryAfterDuration)
|
||||
}
|
||||
|
||||
// default backoff retry policy
|
||||
retryDuration *= 2
|
||||
if retryDuration > maxRetryDuration {
|
||||
retryDuration = maxRetryDuration
|
||||
}
|
||||
|
||||
return retryDuration
|
||||
}
|
||||
|
||||
// parseRetryAfterHeader parses `Retry-After` value retrieved from HTTP response header.
|
||||
// retryAfterString should be in either HTTP-date or a number of seconds.
|
||||
// It will return time.Duration(0) if `retryAfterString` does not follow RFC 7231.
|
||||
func parseRetryAfterHeader(retryAfterString string) (retryAfterDuration time.Duration) {
|
||||
if retryAfterString == "" {
|
||||
return retryAfterDuration
|
||||
}
|
||||
|
||||
defer func() {
|
||||
v := retryAfterDuration.Seconds()
|
||||
logger.Infof("'Retry-After: %s' parsed into %.2f second(s)", retryAfterString, v)
|
||||
}()
|
||||
|
||||
// Retry-After could be in "Mon, 02 Jan 2006 15:04:05 GMT" format.
|
||||
if parsedTime, err := time.Parse(http.TimeFormat, retryAfterString); err == nil {
|
||||
return time.Duration(time.Until(parsedTime).Seconds()) * time.Second
|
||||
}
|
||||
// Retry-After could be in seconds.
|
||||
if seconds, err := strconv.Atoi(retryAfterString); err == nil {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
99
app/vmagent/remotewrite/client_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package remotewrite
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCalculateRetryDuration(t *testing.T) {
|
||||
// `testFunc` call `calculateRetryDuration` for `n` times
|
||||
// and evaluate if the result of `calculateRetryDuration` is
|
||||
// 1. >= expectMinDuration
|
||||
// 2. <= expectMinDuration + 10% (see timeutil.AddJitterToDuration)
|
||||
f := func(retryAfterDuration, retryDuration time.Duration, n int, expectMinDuration time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
retryDuration = getRetryDuration(retryAfterDuration, retryDuration, time.Minute)
|
||||
}
|
||||
|
||||
expectMaxDuration := helper(expectMinDuration)
|
||||
expectMinDuration = expectMinDuration - (1000 * time.Millisecond) // Avoid edge case when calculating time.Until(now)
|
||||
|
||||
if !(retryDuration >= expectMinDuration && retryDuration <= expectMaxDuration) {
|
||||
t.Fatalf(
|
||||
"incorrect retry duration, want (ms): [%d, %d], got (ms): %d",
|
||||
expectMinDuration.Milliseconds(), expectMaxDuration.Milliseconds(),
|
||||
retryDuration.Milliseconds(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Call calculateRetryDuration for 1 time.
|
||||
{
|
||||
// default backoff policy
|
||||
f(0, time.Second, 1, 2*time.Second)
|
||||
// default backoff policy exceed max limit"
|
||||
f(0, 10*time.Minute, 1, time.Minute)
|
||||
|
||||
// retry after > default backoff policy
|
||||
f(10*time.Second, 1*time.Second, 1, 10*time.Second)
|
||||
// retry after < default backoff policy
|
||||
f(1*time.Second, 10*time.Second, 1, 1*time.Second)
|
||||
// retry after invalid and < default backoff policy
|
||||
f(0, time.Second, 1, 2*time.Second)
|
||||
|
||||
}
|
||||
|
||||
// Call calculateRetryDuration for multiple times.
|
||||
{
|
||||
// default backoff policy 2 times
|
||||
f(0, time.Second, 2, 4*time.Second)
|
||||
// default backoff policy 3 times
|
||||
f(0, time.Second, 3, 8*time.Second)
|
||||
// default backoff policy N times exceed max limit
|
||||
f(0, time.Second, 10, time.Minute)
|
||||
|
||||
// retry after 120s 1 times
|
||||
f(120*time.Second, time.Second, 1, 120*time.Second)
|
||||
// retry after 120s 2 times
|
||||
f(120*time.Second, time.Second, 2, 120*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRetryAfterHeader(t *testing.T) {
|
||||
f := func(retryAfterString string, expectResult time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
result := parseRetryAfterHeader(retryAfterString)
|
||||
// expect `expectResult == result` when retryAfterString is in seconds or invalid
|
||||
// expect the difference between result and expectResult to be lower than 10%
|
||||
if !(expectResult == result || math.Abs(float64(expectResult-result))/float64(expectResult) < 0.10) {
|
||||
t.Fatalf(
|
||||
"incorrect retry after duration, want (ms): %d, got (ms): %d",
|
||||
expectResult.Milliseconds(), result.Milliseconds(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// retry after header in seconds
|
||||
f("10", 10*time.Second)
|
||||
// retry after header in date time
|
||||
f(time.Now().Add(30*time.Second).UTC().Format(http.TimeFormat), 30*time.Second)
|
||||
// retry after header invalid
|
||||
f("invalid-retry-after", 0)
|
||||
// retry after header not in GMT
|
||||
f(time.Now().Add(10*time.Second).Format("Mon, 02 Jan 2006 15:04:05 FAKETZ"), 0)
|
||||
}
|
||||
|
||||
// helper calculate the max possible time duration calculated by timeutil.AddJitterToDuration.
|
||||
func helper(d time.Duration) time.Duration {
|
||||
dv := d / 10
|
||||
if dv > 10*time.Second {
|
||||
dv = 10 * time.Second
|
||||
}
|
||||
|
||||
return d + dv
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package remotewrite
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
@@ -23,7 +24,7 @@ var (
|
||||
streamAggrGlobalDropInput = flag.Bool("streamAggr.dropInput", false, "Whether to drop all the input samples after the aggregation "+
|
||||
"with -remoteWrite.streamAggr.config. By default, only aggregates samples are dropped, while the remaining samples "+
|
||||
"are written to remote storages write. See also -streamAggr.keepInput and https://docs.victoriametrics.com/stream-aggregation/")
|
||||
streamAggrGlobalDedupInterval = flagutil.NewDuration("streamAggr.dedupInterval", "0s", "Input samples are de-duplicated with this interval on "+
|
||||
streamAggrGlobalDedupInterval = flag.Duration("streamAggr.dedupInterval", 0, "Input samples are de-duplicated with this interval on "+
|
||||
"aggregator before optional aggregation with -streamAggr.config . "+
|
||||
"See also -dedup.minScrapeInterval and https://docs.victoriametrics.com/stream-aggregation/#deduplication")
|
||||
streamAggrGlobalIgnoreOldSamples = flag.Bool("streamAggr.ignoreOldSamples", false, "Whether to ignore input samples with old timestamps outside the "+
|
||||
@@ -56,6 +57,7 @@ var (
|
||||
"See https://docs.victoriametrics.com/stream-aggregation/#ignore-aggregation-intervals-on-start")
|
||||
streamAggrDropInputLabels = flagutil.NewArrayString("remoteWrite.streamAggr.dropInputLabels", "An optional list of labels to drop from samples "+
|
||||
"before stream de-duplication and aggregation with -remoteWrite.streamAggr.config and -remoteWrite.streamAggr.dedupInterval at the corresponding -remoteWrite.url. "+
|
||||
"Multiple labels per remoteWrite.url must be delimited by '^^': -remoteWrite.streamAggr.dropInputLabels='replica^^az,replica'. "+
|
||||
"See https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels")
|
||||
)
|
||||
|
||||
@@ -131,7 +133,7 @@ func initStreamAggrConfigGlobal() {
|
||||
metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_streamaggr_config_reload_successful{path=%q}`, filePath)).Set(1)
|
||||
metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_streamaggr_config_reload_success_timestamp_seconds{path=%q}`, filePath)).Set(fasttime.UnixTimestamp())
|
||||
}
|
||||
dedupInterval := streamAggrGlobalDedupInterval.Duration()
|
||||
dedupInterval := *streamAggrGlobalDedupInterval
|
||||
if dedupInterval > 0 {
|
||||
deduplicatorGlobal = streamaggr.NewDeduplicator(pushToRemoteStoragesTrackDropped, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
|
||||
}
|
||||
@@ -155,7 +157,11 @@ func (rwctx *remoteWriteCtx) initStreamAggrConfig() {
|
||||
dedupInterval := streamAggrDedupInterval.GetOptionalArg(idx)
|
||||
if dedupInterval > 0 {
|
||||
alias := fmt.Sprintf("dedup-%d", idx+1)
|
||||
rwctx.deduplicator = streamaggr.NewDeduplicator(rwctx.pushInternalTrackDropped, dedupInterval, *streamAggrDropInputLabels, alias)
|
||||
var dropLabels []string
|
||||
if streamAggrDropInputLabels.GetOptionalArg(idx) != "" {
|
||||
dropLabels = strings.Split(streamAggrDropInputLabels.GetOptionalArg(idx), "^^")
|
||||
}
|
||||
rwctx.deduplicator = streamaggr.NewDeduplicator(rwctx.pushInternalTrackDropped, dedupInterval, dropLabels, alias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +202,7 @@ func newStreamAggrConfigGlobal() (*streamaggr.Aggregators, error) {
|
||||
}
|
||||
|
||||
opts := &streamaggr.Options{
|
||||
DedupInterval: streamAggrGlobalDedupInterval.Duration(),
|
||||
DedupInterval: *streamAggrGlobalDedupInterval,
|
||||
DropInputLabels: *streamAggrGlobalDropInputLabels,
|
||||
IgnoreOldSamples: *streamAggrGlobalIgnoreOldSamples,
|
||||
IgnoreFirstIntervals: *streamAggrGlobalIgnoreFirstIntervals,
|
||||
@@ -224,9 +230,13 @@ func newStreamAggrConfigPerURL(idx int, pushFunc streamaggr.PushFunc) (*streamag
|
||||
if *showRemoteWriteURL {
|
||||
alias = fmt.Sprintf("%d:%s", idx+1, remoteWriteURLs.GetOptionalArg(idx))
|
||||
}
|
||||
var dropLabels []string
|
||||
if streamAggrDropInputLabels.GetOptionalArg(idx) != "" {
|
||||
dropLabels = strings.Split(streamAggrDropInputLabels.GetOptionalArg(idx), "^^")
|
||||
}
|
||||
opts := &streamaggr.Options{
|
||||
DedupInterval: streamAggrDedupInterval.GetOptionalArg(idx),
|
||||
DropInputLabels: *streamAggrDropInputLabels,
|
||||
DropInputLabels: dropLabels,
|
||||
IgnoreOldSamples: streamAggrIgnoreOldSamples.GetOptionalArg(idx),
|
||||
IgnoreFirstIntervals: streamAggrIgnoreFirstIntervals.GetOptionalArg(idx),
|
||||
KeepInput: streamAggrKeepInput.GetOptionalArg(idx),
|
||||
|
||||
@@ -8,5 +8,5 @@ FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 8429
|
||||
ENTRYPOINT ["/vmalert-tool-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmalert-tool-linux-${TARGETARCH}-prod ./vmalert-tool-prod
|
||||
|
||||
@@ -43,18 +43,33 @@ func httpWrite(address string, r io.Reader) {
|
||||
// writeInputSeries send input series to vmstorage and flush them
|
||||
func writeInputSeries(input []series, interval *promutils.Duration, startStamp time.Time, dst string) error {
|
||||
r := testutil.WriteRequest{}
|
||||
var err error
|
||||
r.Timeseries, err = parseInputSeries(input, interval, startStamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := testutil.Compress(r)
|
||||
// write input series to vm
|
||||
httpWrite(dst, bytes.NewBuffer(data))
|
||||
vmstorage.Storage.DebugFlush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseInputSeries(input []series, interval *promutils.Duration, startStamp time.Time) ([]testutil.TimeSeries, error) {
|
||||
var res []testutil.TimeSeries
|
||||
for _, data := range input {
|
||||
expr, err := metricsql.Parse(data.Series)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse series %s: %v", data.Series, err)
|
||||
return res, fmt.Errorf("failed to parse series %s: %v", data.Series, err)
|
||||
}
|
||||
promvals, err := parseInputValue(data.Values, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse input series value %s: %v", data.Values, err)
|
||||
return res, fmt.Errorf("failed to parse input series value %s: %v", data.Values, err)
|
||||
}
|
||||
metricExpr, ok := expr.(*metricsql.MetricExpr)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to parse series %s to metric expr: %v", data.Series, err)
|
||||
if !ok || len(metricExpr.LabelFilterss) != 1 {
|
||||
return res, fmt.Errorf("got invalid input series %s: %v", data.Series, err)
|
||||
}
|
||||
samples := make([]testutil.Sample, 0, len(promvals))
|
||||
ts := startStamp
|
||||
@@ -71,14 +86,9 @@ func writeInputSeries(input []series, interval *promutils.Duration, startStamp t
|
||||
for _, filter := range metricExpr.LabelFilterss[0] {
|
||||
ls = append(ls, testutil.Label{Name: filter.Label, Value: filter.Value})
|
||||
}
|
||||
r.Timeseries = append(r.Timeseries, testutil.TimeSeries{Labels: ls, Samples: samples})
|
||||
res = append(res, testutil.TimeSeries{Labels: ls, Samples: samples})
|
||||
}
|
||||
|
||||
data := testutil.Compress(r)
|
||||
// write input series to vm
|
||||
httpWrite(dst, bytes.NewBuffer(data))
|
||||
vmstorage.Storage.DebugFlush()
|
||||
return nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// parseInputValue support input like "1", "1+1x1 _ -4 3+20x1", see more examples in test.
|
||||
|
||||
@@ -2,8 +2,10 @@ package unittest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
)
|
||||
|
||||
func TestParseInputValue_Failure(t *testing.T) {
|
||||
@@ -43,7 +45,7 @@ func TestParseInputValue_Success(t *testing.T) {
|
||||
if decimal.IsStaleNaN(outputExpected[i].Value) && decimal.IsStaleNaN(output[i].Value) {
|
||||
continue
|
||||
}
|
||||
t.Fatalf("unexpeccted Value field in the output\ngot\n%v\nwant\n%v", output, outputExpected)
|
||||
t.Fatalf("unexpected Value field in the output\ngot\n%v\nwant\n%v", output, outputExpected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,3 +66,34 @@ func TestParseInputValue_Success(t *testing.T) {
|
||||
|
||||
f("1+1x1 _ -4 stale 3+20x1", []sequenceValue{{Value: 1}, {Value: 2}, {Omitted: true}, {Value: -4}, {Value: decimal.StaleNaN}, {Value: 3}, {Value: 23}})
|
||||
}
|
||||
|
||||
func TestParseInputSeries_Success(t *testing.T) {
|
||||
f := func(input []series) {
|
||||
t.Helper()
|
||||
var interval promutils.Duration
|
||||
_, err := parseInputSeries(input, &interval, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("expect to see no error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
f([]series{{Series: "test", Values: "1"}})
|
||||
f([]series{{Series: "test{}", Values: "1"}})
|
||||
f([]series{{Series: "test{env=\"prod\",job=\"a\" }", Values: "1"}})
|
||||
f([]series{{Series: "{__name__=\"test\",env=\"prod\",job=\"a\" }", Values: "1"}})
|
||||
}
|
||||
|
||||
func TestParseInputSeries_Fail(t *testing.T) {
|
||||
f := func(input []series) {
|
||||
t.Helper()
|
||||
var interval promutils.Duration
|
||||
_, err := parseInputSeries(input, &interval, time.Now())
|
||||
if err == nil {
|
||||
t.Fatalf("expect to see error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
f([]series{{Series: "", Values: "1"}})
|
||||
f([]series{{Series: "{}", Values: "1"}})
|
||||
f([]series{{Series: "{env=\"prod\",job=\"a\" or env=\"dev\",job=\"b\"}", Values: "1"}})
|
||||
}
|
||||
|
||||
@@ -57,16 +57,18 @@ Outer:
|
||||
continue Outer
|
||||
}
|
||||
metricsqlMetricExpr, ok := metricsqlExpr.(*metricsql.MetricExpr)
|
||||
if !ok {
|
||||
if !ok || len(metricsqlMetricExpr.LabelFilterss) > 1 {
|
||||
checkErrs = append(checkErrs, fmt.Errorf("\n expr: %q, time: %s, err: %v", mt.Expr,
|
||||
mt.EvalTime.Duration().String(), fmt.Errorf("got unsupported metricsql type")))
|
||||
mt.EvalTime.Duration().String(), fmt.Errorf("got invalid exp_samples: %q", s.Labels)))
|
||||
continue Outer
|
||||
}
|
||||
for _, l := range metricsqlMetricExpr.LabelFilterss[0] {
|
||||
expLb = append(expLb, datasource.Label{
|
||||
Name: l.Label,
|
||||
Value: l.Value,
|
||||
})
|
||||
if len(metricsqlMetricExpr.LabelFilterss) > 0 {
|
||||
for _, l := range metricsqlMetricExpr.LabelFilterss[0] {
|
||||
expLb = append(expLb, datasource.Label{
|
||||
Name: l.Label,
|
||||
Value: l.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(expLb, func(i, j int) bool {
|
||||
|
||||
@@ -250,7 +250,7 @@ checkCheck:
|
||||
if readyCheckFunc() {
|
||||
break checkCheck
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config/log"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envtemplate"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Group contains list of Rules grouped into
|
||||
@@ -298,16 +299,30 @@ func parseConfig(data []byte) ([]Group, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot expand environment vars: %w", err)
|
||||
}
|
||||
g := struct {
|
||||
|
||||
var result []Group
|
||||
type cfgFile struct {
|
||||
Groups []Group `yaml:"groups"`
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]any `yaml:",inline"`
|
||||
}{}
|
||||
err = yaml.Unmarshal(data, &g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g.Groups, checkOverflow(g.XXX, "config")
|
||||
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
for {
|
||||
var cf cfgFile
|
||||
if err = decoder.Decode(&cf); err != nil {
|
||||
if err == io.EOF { // EOF indicates no more documents to read
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err = checkOverflow(cf.XXX, "config"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, cf.Groups...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func checkOverflow(m map[string]any, ctx string) error {
|
||||
|
||||
@@ -9,11 +9,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/templates"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -40,6 +39,34 @@ groups:
|
||||
w.Write([]byte(`
|
||||
groups:
|
||||
- name: TestGroup
|
||||
rules:
|
||||
- record: conns
|
||||
expr: max(vm_tcplistener_conns)`))
|
||||
})
|
||||
mux.HandleFunc("/good-multi-doc", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write([]byte(`
|
||||
groups:
|
||||
- name: foo
|
||||
rules:
|
||||
- record: conns
|
||||
expr: max(vm_tcplistener_conns)
|
||||
---
|
||||
groups:
|
||||
- name: bar
|
||||
rules:
|
||||
- record: conns
|
||||
expr: max(vm_tcplistener_conns)`))
|
||||
})
|
||||
mux.HandleFunc("/bad-multi-doc", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Write([]byte(`
|
||||
bad_field:
|
||||
- name: foo
|
||||
rules:
|
||||
- record: conns
|
||||
expr: max(vm_tcplistener_conns)
|
||||
---
|
||||
groups:
|
||||
- name: bar
|
||||
rules:
|
||||
- record: conns
|
||||
expr: max(vm_tcplistener_conns)`))
|
||||
@@ -48,13 +75,23 @@ groups:
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
if _, err := Parse([]string{srv.URL + "/good-alert", srv.URL + "/good-rr"}, notifier.ValidateTemplates, true); err != nil {
|
||||
t.Fatalf("error parsing URLs %s", err)
|
||||
f := func(urls []string, expErr bool) {
|
||||
for i, u := range urls {
|
||||
urls[i] = srv.URL + u
|
||||
}
|
||||
_, err := Parse(urls, notifier.ValidateTemplates, true)
|
||||
if err != nil && !expErr {
|
||||
t.Fatalf("error parsing URLs %s", err)
|
||||
}
|
||||
if err == nil && expErr {
|
||||
t.Fatalf("expecting error parsing URLs but got none")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Parse([]string{srv.URL + "/bad"}, notifier.ValidateTemplates, true); err == nil {
|
||||
t.Fatalf("expected parsing error: %s", err)
|
||||
}
|
||||
f([]string{"/good-alert", "/good-rr", "/good-multi-doc"}, false)
|
||||
f([]string{"/bad"}, true)
|
||||
f([]string{"/bad-multi-doc"}, true)
|
||||
f([]string{"/good-alert", "/bad"}, true)
|
||||
}
|
||||
|
||||
func TestParse_Success(t *testing.T) {
|
||||
@@ -86,6 +123,8 @@ func TestParse_Failure(t *testing.T) {
|
||||
f([]string{"testdata/dir/rules4-bad.rules"}, "either `record` or `alert` must be set")
|
||||
f([]string{"testdata/rules/rules1-bad.rules"}, "bad graphite expr")
|
||||
f([]string{"testdata/dir/rules6-bad.rules"}, "missing ':' in header")
|
||||
f([]string{"testdata/rules/rules-multi-doc-bad.rules"}, "unknown fields")
|
||||
f([]string{"testdata/rules/rules-multi-doc-duplicates-bad.rules"}, "duplicate")
|
||||
f([]string{"http://unreachable-url"}, "failed to")
|
||||
}
|
||||
|
||||
|
||||
29
app/vmalert/config/testdata/rules/rules-multi-doc-bad.rules
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
groups:
|
||||
- name: groupTest
|
||||
rules:
|
||||
- alert: VMRows
|
||||
for: 1ms
|
||||
expr: vm_rows > 0
|
||||
labels:
|
||||
label: bar
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "{{ $value }}"
|
||||
invalid-field-1: invalid-value-1
|
||||
invalid-field-2: invalid-value-2
|
||||
---
|
||||
groups:
|
||||
- name: TestGroup
|
||||
interval: 2s
|
||||
concurrency: 2
|
||||
type: graphite
|
||||
rules:
|
||||
- alert: Conns
|
||||
expr: filterSeries(sumSeries(host.receiver.interface.cons),'last','>', 500)
|
||||
for: 3m
|
||||
|
||||
annotations:
|
||||
summary: Too high connection number for {{$labels.instance}}
|
||||
description: "It is {{ $value }} connections for {{$labels.instance}}"
|
||||
invalid-field-2: invalid-value-2
|
||||
invalid-field-3: invalid-value-3
|
||||
11
app/vmalert/config/testdata/rules/rules-multi-doc-duplicates-bad.rules
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
groups:
|
||||
- name: foo
|
||||
rules:
|
||||
- alert: VMRows
|
||||
expr: vm_rows > 0
|
||||
---
|
||||
groups:
|
||||
- name: foo
|
||||
rules:
|
||||
- alert: VMRows
|
||||
expr: vm_rows > 0
|
||||
15
app/vmalert/config/testdata/rules/rules-multi-doc-good.rules
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
---
|
||||
groups:
|
||||
- name: groupTest
|
||||
rules:
|
||||
- alert: VMRows
|
||||
for: 1ms
|
||||
expr: vm_rows > 0
|
||||
labels:
|
||||
label: bar
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "{{ $value }}"
|
||||
---
|
||||
groups:
|
||||
46
app/vmalert/config/testdata/rules/rules-multi-doc2-good.rules
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
groups:
|
||||
- name: groupTest
|
||||
rules:
|
||||
- alert: VMRows
|
||||
for: 1ms
|
||||
expr: vm_rows > 0
|
||||
labels:
|
||||
label: bar
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "{{ $value }}"
|
||||
- name: groupTest-2
|
||||
rules:
|
||||
- alert: VMRows-2
|
||||
for: 1ms
|
||||
expr: vm_rows_2 > 0
|
||||
labels:
|
||||
label: bar2
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "\n markdown result is : \n---\n # header\n body: \n text \n----\n"
|
||||
---
|
||||
groups:
|
||||
- name: groupTest-3
|
||||
rules:
|
||||
- alert: VMRows-3
|
||||
for: 1ms
|
||||
expr: vm_rows_3 > 0
|
||||
labels:
|
||||
label: bar_3
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "{{ $value }}"
|
||||
- name: groupTest-4
|
||||
rules:
|
||||
- alert: VMRows-4
|
||||
for: 1ms
|
||||
expr: vm_rows_4 > 0
|
||||
labels:
|
||||
label: bar4
|
||||
host: "{{ $labels.instance }}"
|
||||
annotations:
|
||||
summary: "{{ $value }}"
|
||||
---
|
||||
groups:
|
||||
@@ -31,14 +31,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
rulePath = flagutil.NewArrayString("rule", `Path to the files or http url with alerting and/or recording rules.
|
||||
rulePath = flagutil.NewArrayString("rule", `Path to the files or http url with alerting and/or recording rules in YAML format.
|
||||
Supports hierarchical patterns and regexpes.
|
||||
Examples:
|
||||
-rule="/path/to/file". Path to a single file with alerting rules.
|
||||
-rule="http://<some-server-addr>/path/to/rules". HTTP URL to a page with alerting rules.
|
||||
-rule="dir/*.yaml" -rule="/*.yaml" -rule="gcs://vmalert-rules/tenant_%{TENANT_ID}/prod".
|
||||
-rule="dir/**/*.yaml". Includes all the .yaml files in "dir" subfolders recursively.
|
||||
Rule files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.
|
||||
Rule files support YAML multi-document. Files may contain %{ENV_VAR} placeholders, which are substituted by the corresponding env vars.
|
||||
|
||||
Enterprise version of vmalert supports S3 and GCS paths to rules.
|
||||
For example: gs://bucket/path/to/rules, s3://bucket/path/to/rules
|
||||
@@ -76,7 +76,7 @@ absolute path to all .tpl files in root.
|
||||
`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. "+
|
||||
"Pass multiple -label flags in order to add multiple label sets.")
|
||||
"In case of conflicts, original labels are kept with prefix `exported_`.")
|
||||
|
||||
remoteReadIgnoreRestoreErrors = flag.Bool("remoteRead.ignoreRestoreErrors", true, "Whether to ignore errors from remote storage when restoring alerts state on startup. DEPRECATED - this flag has no effect and will be removed in the next releases.")
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@ FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 8880
|
||||
ENTRYPOINT ["/vmalert-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmalert-linux-${TARGETARCH}-prod ./vmalert-prod
|
||||
|
||||
@@ -187,7 +187,7 @@ func templateAnnotation(dst io.Writer, text string, data tplData, tmpl *textTpl.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Alert) toPromLabels(relabelCfg *promrelabel.ParsedConfigs) []prompbmarshal.Label {
|
||||
func (a Alert) applyRelabelingIfNeeded(relabelCfg *promrelabel.ParsedConfigs) []prompbmarshal.Label {
|
||||
var labels []prompbmarshal.Label
|
||||
for k, v := range a.Labels {
|
||||
labels = append(labels, prompbmarshal.Label{
|
||||
|
||||
@@ -187,7 +187,7 @@ func TestAlert_toPromLabels(t *testing.T) {
|
||||
fn := func(labels map[string]string, exp []prompbmarshal.Label, relabel *promrelabel.ParsedConfigs) {
|
||||
t.Helper()
|
||||
a := Alert{Labels: labels}
|
||||
got := a.toPromLabels(relabel)
|
||||
got := a.applyRelabelingIfNeeded(relabel)
|
||||
if !reflect.DeepEqual(got, exp) {
|
||||
t.Fatalf("expected to have: \n%v;\ngot:\n%v",
|
||||
exp, got)
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) %}
|
||||
[
|
||||
{% for i, alert := range alerts %}
|
||||
{% code lbls := alert.applyRelabelingIfNeeded(relabelCfg) %}
|
||||
{% if len(lbls) == 0 %} {% continue %} {% endif %}
|
||||
{
|
||||
"startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %},
|
||||
"generatorURL": {%q= generatorURL(alert) %},
|
||||
@@ -15,7 +17,6 @@
|
||||
"endsAt":{%q= alert.End.Format(time.RFC3339Nano) %},
|
||||
{% endif %}
|
||||
"labels": {
|
||||
{% code lbls := alert.toPromLabels(relabelCfg) %}
|
||||
{% code ll := len(lbls) %}
|
||||
{% for idx, l := range lbls %}
|
||||
{%q= l.Name %}:{%q= l.Value %}{% if idx != ll-1 %}, {% endif %}
|
||||
|
||||
@@ -30,111 +30,117 @@ func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL fun
|
||||
qw422016.N().S(`[`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:10
|
||||
for i, alert := range alerts {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:10
|
||||
qw422016.N().S(`{"startsAt":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
qw422016.N().S(`,"generatorURL":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:13
|
||||
qw422016.N().Q(generatorURL(alert))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:13
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
if !alert.End.IsZero() {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
qw422016.N().S(`"endsAt":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
qw422016.N().S(`"labels": {`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:18
|
||||
lbls := alert.toPromLabels(relabelCfg)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:11
|
||||
lbls := alert.applyRelabelingIfNeeded(relabelCfg)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:19
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
if len(lbls) == 0 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
continue
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
qw422016.N().S(`{"startsAt":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
qw422016.N().S(`,"generatorURL":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
qw422016.N().Q(generatorURL(alert))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
if !alert.End.IsZero() {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
qw422016.N().S(`"endsAt":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:17
|
||||
qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:17
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:18
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:18
|
||||
qw422016.N().S(`"labels": {`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:20
|
||||
ll := len(lbls)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:20
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
for idx, l := range lbls {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
qw422016.N().Q(l.Name)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
qw422016.N().Q(l.Value)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
if idx != ll-1 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:23
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:23
|
||||
qw422016.N().S(`},"annotations": {`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:25
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:26
|
||||
c := len(alert.Annotations)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:26
|
||||
for k, v := range alert.Annotations {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:27
|
||||
for k, v := range alert.Annotations {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
c = c - 1
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
qw422016.N().Q(k)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
qw422016.N().Q(v)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
if c > 0 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:30
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:30
|
||||
qw422016.N().S(`}}`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:32
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
if i != len(alerts)-1 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:32
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:32
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:34
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:34
|
||||
qw422016.N().S(`]`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
}
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
streamamRequest(qw422016, alerts, generatorURL, relabelCfg)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
}
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) string {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
writeamRequest(qb422016, alerts, generatorURL, relabelCfg)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
return qs422016
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
)
|
||||
|
||||
func TestAlertManager_Addr(t *testing.T) {
|
||||
@@ -76,12 +77,33 @@ func TestAlertManager_Send(t *testing.T) {
|
||||
if a[0].EndAt.IsZero() {
|
||||
t.Fatalf("expected non-zero end time")
|
||||
}
|
||||
if len(a[0].Labels) != 1 {
|
||||
t.Fatalf("expected 1 labels got %d", len(a[0].Labels))
|
||||
}
|
||||
if len(a[0].Annotations) != 2 {
|
||||
t.Fatalf("expected 2 annotations got %d", len(a[0].Annotations))
|
||||
}
|
||||
if r.Header.Get(headerKey) != "bar" {
|
||||
t.Fatalf("expected header %q to be set to %q; got %q instead", headerKey, headerValue, r.Header.Get(headerKey))
|
||||
}
|
||||
case 3:
|
||||
if r.Header.Get(headerKey) != headerValue {
|
||||
t.Fatalf("expected header %q to be set to %q; got %q instead", headerKey, headerValue, r.Header.Get(headerKey))
|
||||
var a []struct {
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
||||
t.Fatalf("can not unmarshal data into alert %s", err)
|
||||
}
|
||||
if len(a) != 1 {
|
||||
t.Fatalf("expected 1 alert in array got %d", len(a))
|
||||
}
|
||||
if len(a[0].Labels) != 3 {
|
||||
t.Fatalf("expected 3 labels got %d", len(a[0].Labels))
|
||||
}
|
||||
if a[0].Labels["env"] != "prod" {
|
||||
t.Fatalf("expected env label to be prod during relabeling, got %s", a[0].Labels["env"])
|
||||
}
|
||||
if r.Header.Get(headerKey) != "bar" {
|
||||
t.Fatalf("expected header %q to be set to %q; got %q instead", headerKey, "bar", r.Header.Get(headerKey))
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -95,31 +117,58 @@ func TestAlertManager_Send(t *testing.T) {
|
||||
},
|
||||
Headers: []string{fmt.Sprintf("%s:%s", headerKey, headerValue)},
|
||||
}
|
||||
parsedConfigs, err := promrelabel.ParseRelabelConfigsData([]byte(`
|
||||
- action: drop
|
||||
if: '{tenant="0"}'
|
||||
regex: ".*"
|
||||
- target_label: "env"
|
||||
replacement: "prod"
|
||||
if: '{tenant="1"}'
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error when parse relabeling config: %s", err)
|
||||
}
|
||||
am, err := NewAlertManager(srv.URL+alertManagerPath, func(alert Alert) string {
|
||||
return strconv.FormatUint(alert.GroupID, 10) + "/" + strconv.FormatUint(alert.ID, 10)
|
||||
}, aCfg, nil, 0)
|
||||
}, aCfg, parsedConfigs, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if err := am.Send(context.Background(), []Alert{{}, {}}, nil); err == nil {
|
||||
|
||||
if err := am.Send(context.Background(), []Alert{{Labels: map[string]string{"a": "b"}}}, nil); err == nil {
|
||||
t.Fatalf("expected connection error got nil")
|
||||
}
|
||||
if err := am.Send(context.Background(), []Alert{}, nil); err == nil {
|
||||
|
||||
if err := am.Send(context.Background(), []Alert{{Labels: map[string]string{"a": "b"}}}, nil); err == nil {
|
||||
t.Fatalf("expected wrong http code error got nil")
|
||||
}
|
||||
|
||||
if err := am.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"},
|
||||
Labels: map[string]string{"alertname": "alert0"},
|
||||
Annotations: map[string]string{"a": "b", "c": "d"},
|
||||
}}, map[string]string{headerKey: "bar"}); err != nil {
|
||||
t.Fatalf("unexpected error %s", err)
|
||||
}
|
||||
if c != 2 {
|
||||
t.Fatalf("expected 2 calls(count from zero) to server got %d", c)
|
||||
}
|
||||
if err := am.Send(context.Background(), nil, map[string]string{headerKey: headerValue}); err != nil {
|
||||
|
||||
if err := am.Send(context.Background(), []Alert{
|
||||
// drop tenant0 alert message during relabeling
|
||||
{
|
||||
Name: "alert1",
|
||||
Labels: map[string]string{"rule": "test", "tenant": "0"},
|
||||
},
|
||||
{
|
||||
Name: "alert2",
|
||||
Labels: map[string]string{"rule": "test", "tenant": "1"},
|
||||
},
|
||||
}, map[string]string{headerKey: "bar"}); err != nil {
|
||||
t.Fatalf("unexpected error %s", err)
|
||||
}
|
||||
|
||||
if c != 3 {
|
||||
t.Fatalf("expected 3 calls(count from zero) to server got %d", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/golang/snappy"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
@@ -22,18 +23,19 @@ import (
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var defaultConcurrency = cgroup.AvailableCPUs() * 2
|
||||
|
||||
const (
|
||||
defaultConcurrency = 4
|
||||
defaultMaxBatchSize = 1e3
|
||||
defaultMaxQueueSize = 1e5
|
||||
defaultFlushInterval = 5 * time.Second
|
||||
defaultMaxBatchSize = 1e4
|
||||
defaultMaxQueueSize = 1e6
|
||||
defaultFlushInterval = 2 * time.Second
|
||||
defaultWriteTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
disablePathAppend = flag.Bool("remoteWrite.disablePathAppend", false, "Whether to disable automatic appending of '/api/v1/write' path to the configured -remoteWrite.url.")
|
||||
sendTimeout = flag.Duration("remoteWrite.sendTimeout", 30*time.Second, "Timeout for sending data to the configured -remoteWrite.url.")
|
||||
retryMinInterval = flag.Duration("remoteWrite.retryMinInterval", time.Second, "The minimum delay between retry attempts. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxInterval")
|
||||
retryMinInterval = flag.Duration("remoteWrite.retryMinInterval", time.Second, "The minimum delay between retry attempts. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxTime")
|
||||
retryMaxTime = flag.Duration("remoteWrite.retryMaxTime", time.Second*30, "The max time spent on retry attempts for the failed remote-write request. Change this value if it is expected for remoteWrite.url to be unreachable for more than -remoteWrite.retryMaxTime. See also -remoteWrite.retryMinInterval")
|
||||
)
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ var (
|
||||
|
||||
idleConnectionTimeout = flag.Duration("remoteWrite.idleConnTimeout", 50*time.Second, `Defines a duration for idle (keep-alive connections) to exist. Consider settings this value less to the value of "-http.idleConnTimeout". It must prevent possible "write: broken pipe" and "read: connection reset by peer" errors.`)
|
||||
|
||||
maxQueueSize = flag.Int("remoteWrite.maxQueueSize", 1e5, "Defines the max number of pending datapoints to remote write endpoint")
|
||||
maxBatchSize = flag.Int("remoteWrite.maxBatchSize", 1e3, "Defines max number of timeseries to be flushed at once")
|
||||
concurrency = flag.Int("remoteWrite.concurrency", 1, "Defines number of writers for concurrent writing into remote write endpoint")
|
||||
flushInterval = flag.Duration("remoteWrite.flushInterval", 5*time.Second, "Defines interval of flushes to remote write endpoint")
|
||||
maxQueueSize = flag.Int("remoteWrite.maxQueueSize", defaultMaxQueueSize, "Defines the max number of pending datapoints to remote write endpoint")
|
||||
maxBatchSize = flag.Int("remoteWrite.maxBatchSize", defaultMaxBatchSize, "Defines max number of timeseries to be flushed at once")
|
||||
concurrency = flag.Int("remoteWrite.concurrency", defaultConcurrency, "Defines number of writers for concurrent writing into remote write endpoint")
|
||||
flushInterval = flag.Duration("remoteWrite.flushInterval", defaultFlushInterval, "Defines interval of flushes to remote write endpoint")
|
||||
|
||||
tlsInsecureSkipVerify = flag.Bool("remoteWrite.tlsInsecureSkipVerify", false, "Whether to skip tls verification when connecting to -remoteWrite.url")
|
||||
tlsCertFile = flag.String("remoteWrite.tlsCertFile", "", "Optional path to client-side TLS certificate file to use when connecting to -remoteWrite.url")
|
||||
|
||||
@@ -335,6 +335,8 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
|
||||
// reset to Pending if there are gaps > EvalInterval between DPs
|
||||
a.State = notifier.StatePending
|
||||
a.ActiveAt = at
|
||||
// re-template the annotations as active timestamp is changed
|
||||
_, a.Annotations, _ = ar.expandTemplates(s, qFn, at)
|
||||
a.Start = time.Time{}
|
||||
} else if at.Sub(a.ActiveAt) >= ar.For && a.State != notifier.StateFiring {
|
||||
a.State = notifier.StateFiring
|
||||
|
||||
@@ -289,7 +289,7 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
4: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
})
|
||||
|
||||
f(newTestAlertingRuleWithKeepFiring("for-pending=>firing=>keepfiring=>firing", defaultStep, defaultStep), [][]datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>keepfiring=>firing", defaultStep, 0, defaultStep, nil), [][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
// empty step to keep firing
|
||||
@@ -302,7 +302,7 @@ func TestAlertingRule_Exec(t *testing.T) {
|
||||
3: {{labels: []string{"name", "foo"}, alert: ¬ifier.Alert{State: notifier.StateFiring}}},
|
||||
})
|
||||
|
||||
f(newTestAlertingRuleWithKeepFiring("for-pending=>firing=>keepfiring=>keepfiring=>inactive=>pending=>firing", defaultStep, 2*defaultStep), [][]datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending=>firing=>keepfiring=>keepfiring=>inactive=>pending=>firing", defaultStep, 0, 2*defaultStep, nil), [][]datasource.Metric{
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
{metricWithLabels(t, "name", "foo")},
|
||||
// empty step to keep firing
|
||||
@@ -395,7 +395,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
{State: notifier.StateFiring, ActiveAt: time.Unix(3e3, 0)},
|
||||
}, nil)
|
||||
|
||||
f(newTestAlertingRule("for-pending", time.Second), []datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-pending", time.Second, 0, 0, map[string]string{"activeAt": "{{ $activeAt.UnixMilli }}"}), []datasource.Metric{
|
||||
{Values: []float64{1, 1, 1}, Timestamps: []int64{1, 3, 5}},
|
||||
}, []*notifier.Alert{
|
||||
{State: notifier.StatePending, ActiveAt: time.Unix(1, 0)},
|
||||
@@ -406,7 +406,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.ID(),
|
||||
Name: "for-pending",
|
||||
Labels: map[string]string{"alertname": "for-pending"},
|
||||
Annotations: map[string]string{},
|
||||
Annotations: map[string]string{"activeAt": "5000"},
|
||||
State: notifier.StatePending,
|
||||
ActiveAt: time.Unix(5, 0),
|
||||
Value: 1,
|
||||
@@ -414,7 +414,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
f(newTestAlertingRule("for-firing", 3*time.Second), []datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-firing", 3*time.Second, 0, 0, map[string]string{"activeAt": "{{ $activeAt.UnixMilli }}"}), []datasource.Metric{
|
||||
{Values: []float64{1, 1, 1}, Timestamps: []int64{1, 3, 5}},
|
||||
}, []*notifier.Alert{
|
||||
{State: notifier.StatePending, ActiveAt: time.Unix(1, 0)},
|
||||
@@ -425,7 +425,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.ID(),
|
||||
Name: "for-firing",
|
||||
Labels: map[string]string{"alertname": "for-firing"},
|
||||
Annotations: map[string]string{},
|
||||
Annotations: map[string]string{"activeAt": "1000"},
|
||||
State: notifier.StateFiring,
|
||||
ActiveAt: time.Unix(1, 0),
|
||||
Start: time.Unix(5, 0),
|
||||
@@ -434,7 +434,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
f(newTestAlertingRule("for-hold-pending", time.Second), []datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("for-hold-pending", time.Second, 0, 0, map[string]string{"activeAt": "{{ $activeAt.UnixMilli }}"}), []datasource.Metric{
|
||||
{Values: []float64{1, 1, 1}, Timestamps: []int64{1, 2, 5}},
|
||||
}, []*notifier.Alert{
|
||||
{State: notifier.StatePending, ActiveAt: time.Unix(1, 0)},
|
||||
@@ -445,7 +445,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
GroupID: fakeGroup.ID(),
|
||||
Name: "for-hold-pending",
|
||||
Labels: map[string]string{"alertname": "for-hold-pending"},
|
||||
Annotations: map[string]string{},
|
||||
Annotations: map[string]string{"activeAt": "5000"},
|
||||
State: notifier.StatePending,
|
||||
ActiveAt: time.Unix(5, 0),
|
||||
Value: 1,
|
||||
@@ -453,7 +453,7 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
f(newTestAlertingRuleWithEvalInterval("firing=>inactive=>inactive=>firing=>firing", 0, time.Second), []datasource.Metric{
|
||||
f(newTestAlertingRuleWithCustomFields("firing=>inactive=>inactive=>firing=>firing", 0, time.Second, 0, nil), []datasource.Metric{
|
||||
{Values: []float64{1, 1, 1, 1}, Timestamps: []int64{1, 4, 5, 6}},
|
||||
}, []*notifier.Alert{
|
||||
{State: notifier.StateFiring, ActiveAt: time.Unix(1, 0)},
|
||||
@@ -538,7 +538,6 @@ func TestAlertingRuleExecRange(t *testing.T) {
|
||||
"source": "vm",
|
||||
},
|
||||
},
|
||||
//
|
||||
{
|
||||
State: notifier.StateFiring, ActiveAt: time.Unix(1, 0),
|
||||
Labels: map[string]string{
|
||||
@@ -1036,15 +1035,13 @@ func newTestAlertingRule(name string, waitFor time.Duration) *AlertingRule {
|
||||
return &rule
|
||||
}
|
||||
|
||||
func newTestAlertingRuleWithEvalInterval(name string, waitFor, evalInterval time.Duration) *AlertingRule {
|
||||
rule := newTestAlertingRule(name, waitFor)
|
||||
rule.EvalInterval = evalInterval
|
||||
return rule
|
||||
}
|
||||
|
||||
func newTestAlertingRuleWithKeepFiring(name string, waitFor, keepFiringFor time.Duration) *AlertingRule {
|
||||
func newTestAlertingRuleWithCustomFields(name string, waitFor, evalInterval, keepFiringFor time.Duration, annotation map[string]string) *AlertingRule {
|
||||
rule := newTestAlertingRule(name, waitFor)
|
||||
if evalInterval != 0 {
|
||||
rule.EvalInterval = evalInterval
|
||||
}
|
||||
rule.KeepFiringFor = keepFiringFor
|
||||
rule.Annotations = annotation
|
||||
return rule
|
||||
}
|
||||
|
||||
|
||||
@@ -324,14 +324,28 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
|
||||
g.infof("will start in %v", sleepBeforeStart)
|
||||
|
||||
sleepTimer := time.NewTimer(sleepBeforeStart)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-g.doneCh:
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-sleepTimer.C:
|
||||
randSleep:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case <-g.doneCh:
|
||||
sleepTimer.Stop()
|
||||
return
|
||||
case ng := <-g.updateCh:
|
||||
g.mu.Lock()
|
||||
err := g.updateWith(ng)
|
||||
if err != nil {
|
||||
logger.Errorf("group %q: failed to update: %s", g.Name, err)
|
||||
g.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
g.mu.Unlock()
|
||||
g.infof("reload successfully")
|
||||
case <-sleepTimer.C:
|
||||
break randSleep
|
||||
}
|
||||
}
|
||||
evalTS = evalTS.Add(sleepBeforeStart)
|
||||
}
|
||||
|
||||
@@ -175,6 +175,74 @@ func TestUpdateWith(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateDuringRandSleep(t *testing.T) {
|
||||
// enable rand sleep to test group update during sleep
|
||||
SkipRandSleepOnGroupStart = false
|
||||
defer func() {
|
||||
SkipRandSleepOnGroupStart = true
|
||||
}()
|
||||
rule := AlertingRule{
|
||||
Name: "jobDown",
|
||||
Expr: "up==0",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
g := &Group{
|
||||
Name: "test",
|
||||
Rules: []Rule{
|
||||
&rule,
|
||||
},
|
||||
// big interval ensures big enough randSleep during start process
|
||||
Interval: 100 * time.Hour,
|
||||
updateCh: make(chan *Group),
|
||||
}
|
||||
go g.Start(context.Background(), nil, nil, nil)
|
||||
|
||||
rule1 := AlertingRule{
|
||||
Name: "jobDown",
|
||||
Expr: "up{job=\"vmagent\"}==0",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
g1 := &Group{
|
||||
Rules: []Rule{
|
||||
&rule1,
|
||||
},
|
||||
}
|
||||
g.updateCh <- g1
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
g.mu.RLock()
|
||||
if g.Rules[0].(*AlertingRule).Expr != "up{job=\"vmagent\"}==0" {
|
||||
t.Fatalf("expected to have updated rule expr")
|
||||
}
|
||||
g.mu.RUnlock()
|
||||
|
||||
rule2 := AlertingRule{
|
||||
Name: "jobDown",
|
||||
Expr: "up{job=\"vmagent\"}==0",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "qux",
|
||||
},
|
||||
}
|
||||
g2 := &Group{
|
||||
Rules: []Rule{
|
||||
&rule2,
|
||||
},
|
||||
}
|
||||
g.updateCh <- g2
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
g.mu.RLock()
|
||||
if len(g.Rules[0].(*AlertingRule).Labels) != 2 {
|
||||
t.Fatalf("expected to have updated labels")
|
||||
}
|
||||
g.mu.RUnlock()
|
||||
|
||||
g.Close()
|
||||
}
|
||||
|
||||
func TestGroupStart(t *testing.T) {
|
||||
const (
|
||||
rules = `
|
||||
|
||||
@@ -50,12 +50,12 @@ func TestRequestToCurl(t *testing.T) {
|
||||
f(req, "curl -k -X POST 'https://user:xxxxx@foo.com?query=up&step=10'")
|
||||
|
||||
req = newReq("https://user:pass@foo.com")
|
||||
req.Header.Set("Authorisation", "Bearer 123456")
|
||||
f(req, "curl -k -X POST -H 'Authorisation: <secret>' 'https://user:xxxxx@foo.com'")
|
||||
req.Header.Set("Authorization", "Bearer 123456")
|
||||
f(req, "curl -k -X POST -H 'Authorization: <secret>' 'https://user:xxxxx@foo.com'")
|
||||
|
||||
req = newReq("https://user:pass@foo.com")
|
||||
req.Header.Set("Authorisation", "Basic 123456")
|
||||
f(req, "curl -k -X POST -H 'Authorisation: <secret>' 'https://user:xxxxx@foo.com'")
|
||||
req.Header.Set("Authorization", "Basic 123456")
|
||||
f(req, "curl -k -X POST -H 'Authorization: <secret>' 'https://user:xxxxx@foo.com'")
|
||||
|
||||
req = newReq("https://foo.com")
|
||||
req.Header.Set("My-Password", "mypassword")
|
||||
@@ -66,7 +66,7 @@ func TestRequestToCurl(t *testing.T) {
|
||||
f(req, "curl -k -X POST -H 'Key-For: <secret>' 'https://foo.com'")
|
||||
|
||||
req = newReq("https://foo.com")
|
||||
req.Header.Set("My-Secret-Org", "secret-organisation")
|
||||
req.Header.Set("My-Secret-Org", "secret-organization")
|
||||
f(req, "curl -k -X POST -H 'My-Secret-Org: <secret>' 'https://foo.com'")
|
||||
|
||||
req = newReq("https://foo.com")
|
||||
|
||||
@@ -215,8 +215,10 @@ func recordingToAPI(rr *rule.RecordingRule) apiRule {
|
||||
Updates: rule.GetAllRuleState(rr),
|
||||
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||
GroupName: rr.GroupName,
|
||||
File: rr.File,
|
||||
}
|
||||
if lastState.Err != nil {
|
||||
r.LastError = lastState.Err.Error()
|
||||
|
||||
@@ -1,9 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/rule"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRecordingToApi(t *testing.T) {
|
||||
fq := &datasource.FakeQuerier{}
|
||||
fq.Add(datasource.Metric{
|
||||
Values: []float64{1}, Timestamps: []int64{0},
|
||||
})
|
||||
g := &rule.Group{
|
||||
Name: "group",
|
||||
File: "rules.yaml",
|
||||
Concurrency: 1,
|
||||
}
|
||||
|
||||
entriesLimit := 44
|
||||
rr := rule.NewRecordingRule(fq, g, config.Rule{
|
||||
ID: 1248,
|
||||
Record: "record_name",
|
||||
Expr: "up",
|
||||
Labels: map[string]string{"label": "value"},
|
||||
UpdateEntriesLimit: &entriesLimit,
|
||||
})
|
||||
|
||||
expectedRes := apiRule{
|
||||
Name: "record_name",
|
||||
Query: "up",
|
||||
Labels: map[string]string{"label": "value"},
|
||||
Health: "ok",
|
||||
Type: ruleTypeRecording,
|
||||
DatasourceType: "prometheus",
|
||||
ID: "1248",
|
||||
GroupID: fmt.Sprintf("%d", g.ID()),
|
||||
GroupName: "group",
|
||||
File: "rules.yaml",
|
||||
MaxUpdates: 44,
|
||||
Updates: make([]rule.StateEntry, 0),
|
||||
}
|
||||
|
||||
res := recordingToAPI(rr)
|
||||
|
||||
if !reflect.DeepEqual(res, expectedRes) {
|
||||
t.Fatalf("expected to have: \n%v;\ngot: \n%v", expectedRes, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlValuesToStrings(t *testing.T) {
|
||||
mapQueryParams := map[string][]string{
|
||||
"param1": {"param1"},
|
||||
|
||||
@@ -8,5 +8,5 @@ FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
EXPOSE 8427
|
||||
ENTRYPOINT ["/vmauth-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmauth-linux-${TARGETARCH}-prod ./vmauth-prod
|
||||
|
||||
@@ -7,5 +7,5 @@ RUN apk update && apk upgrade && apk --update --no-cache add ca-certificates
|
||||
FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
ENTRYPOINT ["/vmbackup-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmbackup-linux-${TARGETARCH}-prod ./vmbackup-prod
|
||||
|
||||
@@ -7,5 +7,5 @@ RUN apk update && apk upgrade && apk --update --no-cache add ca-certificates
|
||||
FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
ENTRYPOINT ["/vmctl-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmctl-linux-${TARGETARCH}-prod ./vmctl-prod
|
||||
|
||||
@@ -158,7 +158,7 @@ func (op *otsdbProcessor) do(s queryObj) error {
|
||||
if len(data.Timestamps) < 1 || len(data.Values) < 1 {
|
||||
return nil
|
||||
}
|
||||
labels := make([]vm.LabelPair, len(data.Tags))
|
||||
labels := make([]vm.LabelPair, 0, len(data.Tags))
|
||||
for k, v := range data.Tags {
|
||||
labels = append(labels, vm.LabelPair{Name: k, Value: v})
|
||||
}
|
||||
|
||||
@@ -36,40 +36,37 @@ func TestGetTime_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
// only year
|
||||
f("2019", time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
f("2019Z", time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// year and month
|
||||
f("2019-01", time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
f("2019-01Z", time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// year and not first month
|
||||
f("2019-02", time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC))
|
||||
f("2019-02Z", time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// year, month and day
|
||||
f("2019-02-01", time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC))
|
||||
f("2019-02-01Z", time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// year, month and not first day
|
||||
f("2019-02-10", time.Date(2019, 2, 10, 0, 0, 0, 0, time.UTC))
|
||||
f("2019-02-10Z", time.Date(2019, 2, 10, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// year, month, day and time
|
||||
f("2019-02-02T00", time.Date(2019, 2, 2, 0, 0, 0, 0, time.UTC))
|
||||
f("2019-02-02T00Z", time.Date(2019, 2, 2, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// year, month, day and one hour time
|
||||
f("2019-02-02T01", time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC))
|
||||
f("2019-02-02T01Z", time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC))
|
||||
|
||||
// time with zero minutes
|
||||
f("2019-02-02T01:00", time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC))
|
||||
f("2019-02-02T01:00Z", time.Date(2019, 2, 2, 1, 0, 0, 0, time.UTC))
|
||||
|
||||
// time with one minute
|
||||
f("2019-02-02T01:01", time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC))
|
||||
f("2019-02-02T01:01Z", time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC))
|
||||
|
||||
// time with zero seconds
|
||||
f("2019-02-02T01:01:00", time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC))
|
||||
f("2019-02-02T01:01:00Z", time.Date(2019, 2, 2, 1, 1, 0, 0, time.UTC))
|
||||
|
||||
// timezone with one second
|
||||
f("2019-02-02T01:01:01", time.Date(2019, 2, 2, 1, 1, 1, 0, time.UTC))
|
||||
|
||||
// time with two second and timezone
|
||||
f("2019-07-07T20:01:02Z", time.Date(2019, 7, 7, 20, 1, 02, 0, time.UTC))
|
||||
f("2019-02-02T01:01:01Z", time.Date(2019, 2, 2, 1, 1, 1, 0, time.UTC))
|
||||
|
||||
// time with seconds and timezone
|
||||
f("2019-07-07T20:47:40+03:00", func() time.Time {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
|
||||
remote_read_integration "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
|
||||
@@ -251,7 +251,7 @@ func deleteSeries(name, value string) (int, error) {
|
||||
if err := tfs.Add([]byte(name), []byte(value), false, true); err != nil {
|
||||
return 0, fmt.Errorf("unexpected error in TagFilters.Add: %w", err)
|
||||
}
|
||||
return vmstorage.DeleteSeries(nil, []*storage.TagFilters{tfs})
|
||||
return vmstorage.DeleteSeries(nil, []*storage.TagFilters{tfs}, 1e3)
|
||||
}
|
||||
|
||||
func TestBuildMatchWithFilter_Failure(t *testing.T) {
|
||||
|
||||
@@ -34,7 +34,7 @@ var (
|
||||
//
|
||||
// See https://github.com/influxdata/telegraf/tree/master/plugins/inputs/socket_listener/
|
||||
func InsertHandlerForReader(r io.Reader) error {
|
||||
return stream.Parse(r, false, "", "", func(db string, rows []parser.Row) error {
|
||||
return stream.Parse(r, true, false, "", "", func(db string, rows []parser.Row) error {
|
||||
return insertRows(db, rows, nil)
|
||||
})
|
||||
}
|
||||
@@ -48,11 +48,12 @@ func InsertHandlerForHTTP(req *http.Request) error {
|
||||
return err
|
||||
}
|
||||
isGzipped := req.Header.Get("Content-Encoding") == "gzip"
|
||||
isStreamMode := req.Header.Get("Stream-Mode") == "1"
|
||||
q := req.URL.Query()
|
||||
precision := q.Get("precision")
|
||||
// Read db tag from https://docs.influxdata.com/influxdb/v1.7/tools/api/#write-http-endpoint
|
||||
db := q.Get("db")
|
||||
return stream.Parse(req.Body, isGzipped, precision, db, func(db string, rows []parser.Row) error {
|
||||
return stream.Parse(req.Body, isStreamMode, isGzipped, precision, db, func(db string, rows []parser.Row) error {
|
||||
return insertRows(db, rows, extraLabels)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ RUN apk update && apk upgrade && apk --update --no-cache add ca-certificates
|
||||
FROM $root_image
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
ENTRYPOINT ["/vmrestore-prod"]
|
||||
ARG TARGETARCH=non-existing
|
||||
ARG TARGETARCH
|
||||
COPY vmrestore-linux-${TARGETARCH}-prod ./vmrestore-prod
|
||||
|
||||
@@ -2,7 +2,6 @@ package vmselect
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -45,10 +44,7 @@ var (
|
||||
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
|
||||
|
||||
func getDefaultMaxConcurrentRequests() int {
|
||||
n := cgroup.AvailableCPUs()
|
||||
if n <= 4 {
|
||||
n *= 2
|
||||
}
|
||||
n := cgroup.AvailableCPUs() * 2
|
||||
if n > 16 {
|
||||
// A single request can saturate all the CPU cores, so there is no sense
|
||||
// in allowing higher number of concurrent requests - they will just contend
|
||||
@@ -64,6 +60,7 @@ func Init() {
|
||||
fs.RemoveDirContents(tmpDirPath)
|
||||
netstorage.InitTmpBlocksDir(tmpDirPath)
|
||||
promql.InitRollupResultCache(*vmstorage.DataPath + "/cache/rollupResult")
|
||||
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
|
||||
|
||||
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
|
||||
initVMAlertProxy()
|
||||
@@ -86,6 +83,9 @@ var (
|
||||
_ = metrics.NewGauge(`vm_concurrent_select_current`, func() float64 {
|
||||
return float64(len(concurrencyLimitCh))
|
||||
})
|
||||
_ = metrics.NewGauge(`vm_search_max_unique_timeseries`, func() float64 {
|
||||
return float64(prometheus.GetMaxUniqueTimeSeries())
|
||||
})
|
||||
)
|
||||
|
||||
//go:embed vmui
|
||||
@@ -187,7 +187,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.LabelValuesHandler(qt, startTime, labelName, w, r); err != nil {
|
||||
labelValuesErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -210,7 +210,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.QueryHandler(qt, startTime, w, r); err != nil {
|
||||
queryErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -219,7 +219,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.QueryRangeHandler(qt, startTime, w, r); err != nil {
|
||||
queryRangeErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -228,7 +228,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.SeriesHandler(qt, startTime, w, r); err != nil {
|
||||
seriesErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -237,7 +237,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.SeriesCountHandler(startTime, w, r); err != nil {
|
||||
seriesCountErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -246,7 +246,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.LabelsHandler(qt, startTime, w, r); err != nil {
|
||||
labelsErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -255,7 +255,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.TSDBStatusHandler(qt, startTime, w, r); err != nil {
|
||||
statusTSDBErrors.Inc()
|
||||
sendPrometheusError(w, r, err)
|
||||
httpserver.SendPrometheusError(w, r, err)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -498,7 +498,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
|
||||
httpserver.EnableCORS(w, r)
|
||||
if err := prometheus.QueryStatsHandler(w, r); err != nil {
|
||||
topQueriesErrors.Inc()
|
||||
sendPrometheusError(w, r, fmt.Errorf("cannot query status endpoint: %w", err))
|
||||
httpserver.SendPrometheusError(w, r, fmt.Errorf("cannot query status endpoint: %w", err))
|
||||
return true
|
||||
}
|
||||
return true
|
||||
@@ -575,24 +575,6 @@ func isGraphiteTagsPath(path string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func sendPrometheusError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
logger.WarnfSkipframes(1, "error in %q: %s", httpserver.GetRequestURI(r), err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
statusCode := http.StatusUnprocessableEntity
|
||||
var esc *httpserver.ErrorWithStatusCode
|
||||
if errors.As(err, &esc) {
|
||||
statusCode = esc.StatusCode
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
var ure *promql.UserReadableError
|
||||
if errors.As(err, &ure) {
|
||||
err = ure
|
||||
}
|
||||
prometheus.WriteErrorResponse(w, statusCode, err)
|
||||
}
|
||||
|
||||
var (
|
||||
requestDuration = metrics.NewHistogram(`vmselect_request_duration_seconds`)
|
||||
|
||||
|
||||
@@ -765,7 +765,7 @@ func putSortBlocksHeap(sbh *sortBlocksHeap) {
|
||||
|
||||
var sbhPool sync.Pool
|
||||
|
||||
// DeleteSeries deletes time series matching the given tagFilterss.
|
||||
// DeleteSeries deletes time series matching the given search query.
|
||||
func DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutils.Deadline) (int, error) {
|
||||
qt = qt.NewChild("delete series: %s", sq)
|
||||
defer qt.Done()
|
||||
@@ -774,7 +774,7 @@ func DeleteSeries(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline sear
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return vmstorage.DeleteSeries(qt, tfss)
|
||||
return vmstorage.DeleteSeries(qt, tfss, sq.MaxMetrics)
|
||||
}
|
||||
|
||||
// LabelNames returns label names matching the given sq until the given deadline.
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Code generated by qtc from "error_response.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
// ErrorResponse generates error response for /api/v1/query.See https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
|
||||
|
||||
//line app/vmselect/prometheus/error_response.qtpl:4
|
||||
package prometheus
|
||||
|
||||
//line app/vmselect/prometheus/error_response.qtpl:4
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line app/vmselect/prometheus/error_response.qtpl:4
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line app/vmselect/prometheus/error_response.qtpl:4
|
||||
func StreamErrorResponse(qw422016 *qt422016.Writer, statusCode int, err error) {
|
||||
//line app/vmselect/prometheus/error_response.qtpl:4
|
||||
qw422016.N().S(`{"status":"error","errorType":"`)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:7
|
||||
qw422016.N().D(statusCode)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:7
|
||||
qw422016.N().S(`","error":`)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:8
|
||||
qw422016.N().Q(err.Error())
|
||||
//line app/vmselect/prometheus/error_response.qtpl:8
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
func WriteErrorResponse(qq422016 qtio422016.Writer, statusCode int, err error) {
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
StreamErrorResponse(qw422016, statusCode, err)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
func ErrorResponse(statusCode int, err error) string {
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
WriteErrorResponse(qb422016, statusCode, err)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/error_response.qtpl:10
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package prometheus
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
"math"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@@ -13,6 +12,10 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
"github.com/valyala/fastjson/fastfloat"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
|
||||
@@ -24,10 +27,10 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
"github.com/valyala/fastjson/fastfloat"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -47,11 +50,13 @@ var (
|
||||
maxStepForPointsAdjustment = flag.Duration("search.maxStepForPointsAdjustment", time.Minute, "The maximum step when /api/v1/query_range handler adjusts "+
|
||||
"points with timestamps closer than -search.latencyOffset to the current time. The adjustment is needed because such points may contain incomplete data")
|
||||
|
||||
maxUniqueTimeseries = flag.Int("search.maxUniqueTimeseries", 300e3, "The maximum number of unique time series, which can be selected during /api/v1/query and /api/v1/query_range queries. This option allows limiting memory usage")
|
||||
maxUniqueTimeseries = flag.Int("search.maxUniqueTimeseries", 0, "The maximum number of unique time series, which can be selected during /api/v1/query and /api/v1/query_range queries. This option allows limiting memory usage. "+
|
||||
"When set to zero, the limit is automatically calculated based on -search.maxConcurrentRequests (inversely proportional) and memory available to the process (proportional).")
|
||||
maxFederateSeries = flag.Int("search.maxFederateSeries", 1e6, "The maximum number of time series, which can be returned from /federate. This option allows limiting memory usage")
|
||||
maxExportSeries = flag.Int("search.maxExportSeries", 10e6, "The maximum number of time series, which can be returned from /api/v1/export* APIs. This option allows limiting memory usage")
|
||||
maxTSDBStatusSeries = flag.Int("search.maxTSDBStatusSeries", 10e6, "The maximum number of time series, which can be processed during the call to /api/v1/status/tsdb. This option allows limiting memory usage")
|
||||
maxSeriesLimit = flag.Int("search.maxSeries", 30e3, "The maximum number of time series, which can be returned from /api/v1/series. This option allows limiting memory usage")
|
||||
maxDeleteSeries = flag.Int("search.maxDeleteSeries", 1e6, "The maximum number of time series, which can be deleted using /api/v1/admin/tsdb/delete_series. This option allows limiting memory usage")
|
||||
maxLabelsAPISeries = flag.Int("search.maxLabelsAPISeries", 1e6, "The maximum number of time series, which could be scanned when searching for the matching time series "+
|
||||
"at /api/v1/labels and /api/v1/label/.../values. This option allows limiting memory usage and CPU usage. See also -search.maxLabelsAPIDuration, "+
|
||||
"-search.maxTagKeys, -search.maxTagValues and -search.ignoreExtraFiltersAtLabelsAPI")
|
||||
@@ -479,7 +484,7 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
|
||||
if !cp.IsDefaultTimeRange() {
|
||||
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
|
||||
}
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, 0)
|
||||
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxDeleteSeries)
|
||||
deletedCount, err := netstorage.DeleteSeries(nil, sq, cp.deadline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete time series: %w", err)
|
||||
@@ -791,7 +796,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
|
||||
End: start,
|
||||
Step: step,
|
||||
MaxPointsPerSeries: *maxPointsPerTimeseries,
|
||||
MaxSeries: *maxUniqueTimeseries,
|
||||
MaxSeries: GetMaxUniqueTimeSeries(),
|
||||
QuotedRemoteAddr: httpserver.GetQuotedRemoteAddr(r),
|
||||
Deadline: deadline,
|
||||
MayCache: mayCache,
|
||||
@@ -899,7 +904,7 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
|
||||
End: end,
|
||||
Step: step,
|
||||
MaxPointsPerSeries: *maxPointsPerTimeseries,
|
||||
MaxSeries: *maxUniqueTimeseries,
|
||||
MaxSeries: GetMaxUniqueTimeSeries(),
|
||||
QuotedRemoteAddr: httpserver.GetQuotedRemoteAddr(r),
|
||||
Deadline: deadline,
|
||||
MayCache: mayCache,
|
||||
@@ -1245,3 +1250,40 @@ func (sw *scalableWriter) flush() error {
|
||||
})
|
||||
return sw.bw.Flush()
|
||||
}
|
||||
|
||||
var (
|
||||
maxUniqueTimeseriesValueOnce sync.Once
|
||||
maxUniqueTimeseriesValue int
|
||||
)
|
||||
|
||||
// InitMaxUniqueTimeseries init the max metrics limit calculated by available resources.
|
||||
// The calculation is split into calculateMaxUniqueTimeSeriesForResource for unit testing.
|
||||
func InitMaxUniqueTimeseries(maxConcurrentRequests int) {
|
||||
maxUniqueTimeseriesValueOnce.Do(func() {
|
||||
maxUniqueTimeseriesValue = *maxUniqueTimeseries
|
||||
if maxUniqueTimeseriesValue <= 0 {
|
||||
maxUniqueTimeseriesValue = calculateMaxUniqueTimeSeriesForResource(maxConcurrentRequests, memory.Remaining())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// calculateMaxUniqueTimeSeriesForResource calculate the max metrics limit calculated by available resources.
|
||||
func calculateMaxUniqueTimeSeriesForResource(maxConcurrentRequests, remainingMemory int) int {
|
||||
if maxConcurrentRequests <= 0 {
|
||||
// This line should NOT be reached unless the user has set an incorrect `search.maxConcurrentRequests`.
|
||||
// In such cases, fallback to unlimited.
|
||||
logger.Warnf("limiting -search.maxUniqueTimeseries to %v because -search.maxConcurrentRequests=%d.", 2e9, maxConcurrentRequests)
|
||||
return 2e9
|
||||
}
|
||||
|
||||
// Calculate the max metrics limit for a single request in the worst-case concurrent scenario.
|
||||
// The approximate size of 1 unique series that could occupy in the vmstorage is 200 bytes.
|
||||
mts := remainingMemory / 200 / maxConcurrentRequests
|
||||
logger.Infof("limiting -search.maxUniqueTimeseries to %d according to -search.maxConcurrentRequests=%d and remaining memory=%d bytes. To increase the limit, reduce -search.maxConcurrentRequests or increase memory available to the process.", mts, maxConcurrentRequests, remainingMemory)
|
||||
return mts
|
||||
}
|
||||
|
||||
// GetMaxUniqueTimeSeries returns the max metrics limit calculated by available resources.
|
||||
func GetMaxUniqueTimeSeries() int {
|
||||
return maxUniqueTimeseriesValue
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"math"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
@@ -229,3 +230,29 @@ func TestGetLatencyOffsetMillisecondsFailure(t *testing.T) {
|
||||
}
|
||||
f("http://localhost?latency_offset=foobar")
|
||||
}
|
||||
|
||||
func TestCalculateMaxMetricsLimitByResource(t *testing.T) {
|
||||
f := func(maxConcurrentRequest, remainingMemory, expect int) {
|
||||
t.Helper()
|
||||
maxMetricsLimit := calculateMaxUniqueTimeSeriesForResource(maxConcurrentRequest, remainingMemory)
|
||||
if maxMetricsLimit != expect {
|
||||
t.Fatalf("unexpected max metrics limit: got %d, want %d", maxMetricsLimit, expect)
|
||||
}
|
||||
}
|
||||
|
||||
// Skip when GOARCH=386
|
||||
if runtime.GOARCH != "386" {
|
||||
// 8 CPU & 32 GiB
|
||||
f(16, int(math.Round(32*1024*1024*1024*0.4)), 4294967)
|
||||
// 4 CPU & 32 GiB
|
||||
f(8, int(math.Round(32*1024*1024*1024*0.4)), 8589934)
|
||||
}
|
||||
|
||||
// 2 CPU & 4 GiB
|
||||
f(4, int(math.Round(4*1024*1024*1024*0.4)), 2147483)
|
||||
|
||||
// other edge cases
|
||||
f(0, int(math.Round(4*1024*1024*1024*0.4)), 2e9)
|
||||
f(4, 0, 0)
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
@@ -43,7 +44,7 @@ var (
|
||||
"See also -search.logSlowQueryDuration and -search.maxMemoryPerQuery")
|
||||
noStaleMarkers = flag.Bool("search.noStaleMarkers", false, "Set this flag to true if the database doesn't contain Prometheus stale markers, "+
|
||||
"so there is no need in spending additional CPU time on its handling. Staleness markers may exist only in data obtained from Prometheus scrape targets")
|
||||
minWindowForInstantRollupOptimization = flagutil.NewDuration("search.minWindowForInstantRollupOptimization", "3h", "Enable cache-based optimization for repeated queries "+
|
||||
minWindowForInstantRollupOptimization = flag.Duration("search.minWindowForInstantRollupOptimization", time.Hour*3, "Enable cache-based optimization for repeated queries "+
|
||||
"to /api/v1/query (aka instant queries), which contain rollup functions with lookbehind window exceeding the given value")
|
||||
)
|
||||
|
||||
@@ -354,7 +355,7 @@ func evalExprInternal(qt *querytracer.Tracer, ec *EvalConfig, e metricsql.Expr)
|
||||
func evalTransformFunc(qt *querytracer.Tracer, ec *EvalConfig, fe *metricsql.FuncExpr) ([]*timeseries, error) {
|
||||
tf := getTransformFunc(fe.Name)
|
||||
if tf == nil {
|
||||
return nil, &UserReadableError{
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: fmt.Errorf(`unknown func %q`, fe.Name),
|
||||
}
|
||||
}
|
||||
@@ -376,7 +377,7 @@ func evalTransformFunc(qt *querytracer.Tracer, ec *EvalConfig, fe *metricsql.Fun
|
||||
}
|
||||
rv, err := tf(tfa)
|
||||
if err != nil {
|
||||
return nil, &UserReadableError{
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: fmt.Errorf(`cannot evaluate %q: %w`, fe.AppendString(nil), err),
|
||||
}
|
||||
}
|
||||
@@ -407,7 +408,7 @@ func evalAggrFunc(qt *querytracer.Tracer, ec *EvalConfig, ae *metricsql.AggrFunc
|
||||
}
|
||||
af := getAggrFunc(ae.Name)
|
||||
if af == nil {
|
||||
return nil, &UserReadableError{
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: fmt.Errorf(`unknown func %q`, ae.Name),
|
||||
}
|
||||
}
|
||||
@@ -802,12 +803,12 @@ func evalRollupFunc(qt *querytracer.Tracer, ec *EvalConfig, funcName string, rf
|
||||
}
|
||||
tssAt, err := evalExpr(qt, ec, re.At)
|
||||
if err != nil {
|
||||
return nil, &UserReadableError{
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: fmt.Errorf("cannot evaluate `@` modifier: %w", err),
|
||||
}
|
||||
}
|
||||
if len(tssAt) != 1 {
|
||||
return nil, &UserReadableError{
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: fmt.Errorf("`@` modifier must return a single series; it returns %d series instead", len(tssAt)),
|
||||
}
|
||||
}
|
||||
@@ -869,7 +870,7 @@ func evalRollupFuncWithoutAt(qt *querytracer.Tracer, ec *EvalConfig, funcName st
|
||||
rvs, err = evalRollupFuncWithSubquery(qt, ecNew, funcName, rf, expr, re)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, &UserReadableError{
|
||||
return nil, &httpserver.UserReadableError{
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
@@ -1091,7 +1092,6 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||
again:
|
||||
offset := int64(0)
|
||||
tssCached := rollupResultCacheV.GetInstantValues(qt, expr, window, ec.Step, ec.EnforcedTagFilterss)
|
||||
ec.QueryStats.addSeriesFetched(len(tssCached))
|
||||
if len(tssCached) == 0 {
|
||||
// Cache miss. Re-populate the missing data.
|
||||
start := int64(fasttime.UnixTimestamp()*1000) - cacheTimestampOffset.Milliseconds()
|
||||
@@ -1135,6 +1135,7 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||
deleteCachedSeries(qt)
|
||||
goto again
|
||||
}
|
||||
ec.QueryStats.addSeriesFetched(len(tssCached))
|
||||
return tssCached, offset, nil
|
||||
}
|
||||
|
||||
@@ -1601,7 +1602,7 @@ func evalRollupFuncWithMetricExpr(qt *querytracer.Tracer, ec *EvalConfig, funcNa
|
||||
if ec.Start == ec.End {
|
||||
rvs, err := evalInstantRollup(qt, ec, funcName, rf, expr, me, iafc, window)
|
||||
if err != nil {
|
||||
err = &UserReadableError{
|
||||
err = &httpserver.UserReadableError{
|
||||
Err: err,
|
||||
}
|
||||
return nil, err
|
||||
@@ -1612,7 +1613,7 @@ func evalRollupFuncWithMetricExpr(qt *querytracer.Tracer, ec *EvalConfig, funcNa
|
||||
evalWithConfig := func(ec *EvalConfig) ([]*timeseries, error) {
|
||||
tss, err := evalRollupFuncNoCache(qt, ec, funcName, rf, expr, me, iafc, window, pointsPerSeries)
|
||||
if err != nil {
|
||||
err = &UserReadableError{
|
||||
err = &httpserver.UserReadableError{
|
||||
Err: err,
|
||||
}
|
||||
return nil, err
|
||||
@@ -1646,6 +1647,10 @@ func evalRollupFuncWithMetricExpr(qt *querytracer.Tracer, ec *EvalConfig, funcNa
|
||||
ecNew = copyEvalConfig(ec)
|
||||
ecNew.Start = start
|
||||
}
|
||||
// call to evalWithConfig also updates QueryStats.addSeriesFetched
|
||||
// without checking whether tss has intersection with tssCached.
|
||||
// So final number could be bigger than actual number of unique series.
|
||||
// This discrepancy is acceptable, since seriesFetched stat is used as info only.
|
||||
tss, err := evalWithConfig(ecNew)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -35,24 +35,6 @@ var (
|
||||
"Such conversion can be disabled using -search.disableImplicitConversion.")
|
||||
)
|
||||
|
||||
// UserReadableError is a type of error which supposed to be returned to the user without additional context.
|
||||
type UserReadableError struct {
|
||||
// Err is the error which needs to be returned to the user.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Unwrap returns ure.Err.
|
||||
//
|
||||
// This is used by standard errors package. See https://golang.org/pkg/errors
|
||||
func (ure *UserReadableError) Unwrap() error {
|
||||
return ure.Err
|
||||
}
|
||||
|
||||
// Error satisfies Error interface
|
||||
func (ure *UserReadableError) Error() string {
|
||||
return ure.Err.Error()
|
||||
}
|
||||
|
||||
// Exec executes q for the given ec.
|
||||
func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly bool) ([]netstorage.Result, error) {
|
||||
if querystats.Enabled() {
|
||||
|
||||
@@ -3027,6 +3027,12 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`1 and (0 > 1)`, func(t *testing.T) {
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6637
|
||||
t.Parallel()
|
||||
q := `1 and (0 > 1)`
|
||||
f(q, nil)
|
||||
})
|
||||
t.Run(`time() and 2`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `time() and 2`
|
||||
@@ -7245,6 +7251,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_trim_outliers(time() > 1200)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_trim_outliers(0.5, time() > 1200)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_trim_spikes()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_trim_spikes(0.2, time())`
|
||||
@@ -7256,6 +7273,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_trim_spikes(time() > 1200 <= 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_trim_spikes(0.2, time() > 1200 <= 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_trim_zscore()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_trim_zscore(0.9, time())`
|
||||
@@ -7267,6 +7295,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_trim_zscore(time() > 1200 <= 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_trim_zscore(0.9, time() > 1200 <= 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_zscore()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(range_zscore(time()), 0.1)`
|
||||
@@ -7278,6 +7317,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_zscore(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(range_zscore(time() > 1200 < 1800), 0.1)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, -1, 1, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_quantile(0.5)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_quantile(0.5, time())`
|
||||
@@ -7289,6 +7339,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_quantile(0.5, time() > 1200 < 2000)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_quantile(0.5, time() > 1200 < 2000)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1600, 1600, 1600, 1600, 1600, 1600},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_stddev()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(range_stddev(time()),0.01)`
|
||||
@@ -7300,6 +7361,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_stddev(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(range_stddev(time() > 1200 < 1800),0.01)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{100, 100, 100, 100, 100, 100},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_stdvar()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(range_stdvar(time()),0.01)`
|
||||
@@ -7311,6 +7383,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_stdvar(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(range_stdvar(time() > 1200 < 1800),0.01)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{10000, 10000, 10000, 10000, 10000, 10000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_median()`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_median(time())`
|
||||
@@ -7671,6 +7754,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_min(abs(1500-time()) < 400 > 100)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_min(abs(1500-time()) < 400 > 100)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, 300, 300, 300, 300, 300},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_max(abs(1300-time()))`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_max(abs(1300-time()))`
|
||||
@@ -7682,6 +7776,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_max(abs(1300-time()) > 300 < 700)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_max(abs(1300-time()) > 300 < 700)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, nan, 500, 500},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_sum(1)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_sum(1)`
|
||||
@@ -7704,6 +7809,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_sum(time() > 1.2 < 1.8)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_sum(time()/1e3 > 1.2 < 1.8)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1.4, 3, 3, 3},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_avg(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_avg(time())`
|
||||
@@ -7715,6 +7831,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`running_avg(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `running_avg(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1500, 1500, 1500},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`smooth_exponential(time(), 1)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `smooth_exponential(time(), 1)`
|
||||
@@ -7795,6 +7922,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_min(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_min(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1400, 1400, 1400, 1400, 1400, 1400},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_normalize(time(),alias(-time(),"negative"))`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_normalize(time(),alias(-time(), "negative"))`
|
||||
@@ -7812,6 +7950,23 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_normalize(time() > 1200 < 1800,alias(-(time() > 1400 < 2000),"negative"))`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_normalize(time() > 1200 < 1800,alias(-(time() > 1200 < 2000), "negative"))`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 0, 1, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1, 0.5, 0, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.MetricGroup = []byte("negative")
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_first(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_first(time())`
|
||||
@@ -7823,6 +7978,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_first(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_first(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1400, 1400, 1400, 1400, 1400, 1400},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_mad(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_mad(time())`
|
||||
@@ -7834,6 +8000,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_mad(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_mad(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{100, 100, 100, 100, 100, 100},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_max(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_max(time())`
|
||||
@@ -7845,6 +8022,39 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_max(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_max(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1600, 1600, 1600, 1600, 1600, 1600},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_sum(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_sum(time())`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{9000, 9000, 9000, 9000, 9000, 9000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_sum(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_sum(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{3000, 3000, 3000, 3000, 3000, 3000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_last(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_last(time())`
|
||||
@@ -7856,6 +8066,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_last(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_last(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1600, 1600, 1600, 1600, 1600, 1600},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_linear_regression(time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_linear_regression(time())`
|
||||
@@ -7878,6 +8099,17 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_linear_regression(time() > 1200 < 1800)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `range_linear_regression(time() > 1200 < 1800)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`range_linear_regression(100/time())`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `sort_desc(round((
|
||||
|
||||
@@ -1544,10 +1544,8 @@ func transformRangeFirst(tfa *transformFuncArg) ([]*timeseries, error) {
|
||||
continue
|
||||
}
|
||||
vFirst := values[0]
|
||||
for i, v := range values {
|
||||
if math.IsNaN(v) {
|
||||
continue
|
||||
}
|
||||
values = ts.Values
|
||||
for i := range values {
|
||||
values[i] = vFirst
|
||||
}
|
||||
}
|
||||
@@ -1571,10 +1569,8 @@ func setLastValues(tss []*timeseries) {
|
||||
continue
|
||||
}
|
||||
vLast := values[len(values)-1]
|
||||
for i, v := range values {
|
||||
if math.IsNaN(v) {
|
||||
continue
|
||||
}
|
||||
values = ts.Values
|
||||
for i := range values {
|
||||
values[i] = vLast
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.81c6ec3a.css",
|
||||
"main.js": "./static/js/main.9ed27532.js",
|
||||
"static/js/685.bebe1265.chunk.js": "./static/js/685.bebe1265.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.8c2e588d62b87f90dbf0.md",
|
||||
"main.css": "./static/css/main.d781989c.css",
|
||||
"main.js": "./static/js/main.68e2aae8.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.81c6ec3a.css",
|
||||
"static/js/main.9ed27532.js"
|
||||
"static/css/main.d781989c.css",
|
||||
"static/js/main.68e2aae8.js"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
1
app/vmselect/vmui/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0" fill="#020202"/><path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z" fill="#020202"/><path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z" fill="#020202"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |