mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-01 08:02:53 +03:00
Compare commits
147 Commits
v1.110.7
...
make/use-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6340fe83 | ||
|
|
1334eee433 | ||
|
|
2d67e14d59 | ||
|
|
539498058e | ||
|
|
b3d22403eb | ||
|
|
0fce51e3b4 | ||
|
|
9e118fe1ee | ||
|
|
3553c60399 | ||
|
|
9b54bd6e8d | ||
|
|
a90edc71c7 | ||
|
|
83deddc84c | ||
|
|
434cb7028c | ||
|
|
107b6517b7 | ||
|
|
f68f5b3113 | ||
|
|
d49b4a7550 | ||
|
|
bd8b4eb78b | ||
|
|
41558066db | ||
|
|
af064ca65a | ||
|
|
eced71a96d | ||
|
|
23fd269ccf | ||
|
|
1f5d02e059 | ||
|
|
690aaf7d2d | ||
|
|
1e0f7f0d28 | ||
|
|
c7a16e1df6 | ||
|
|
2cb909022f | ||
|
|
fe70b963e4 | ||
|
|
9bb726751c | ||
|
|
3c85ffb1e6 | ||
|
|
65cb6468ac | ||
|
|
8e645ea708 | ||
|
|
b95bdb5781 | ||
|
|
5ecc5770c2 | ||
|
|
02c03793b3 | ||
|
|
c74c4b24d7 | ||
|
|
07be0c6129 | ||
|
|
826c408e0e | ||
|
|
913b64d9b5 | ||
|
|
6b76dead5a | ||
|
|
41991edb34 | ||
|
|
eb7c21bde5 | ||
|
|
3cc8013dd9 | ||
|
|
1209f33c6d | ||
|
|
3c87e361ba | ||
|
|
f5c9c5bf01 | ||
|
|
7712a34ba6 | ||
|
|
d890bf52fe | ||
|
|
f52478dac7 | ||
|
|
bcc2c85e53 | ||
|
|
001f9218b1 | ||
|
|
f7fc897f85 | ||
|
|
e58b512305 | ||
|
|
d33efbbd95 | ||
|
|
23cb0475e9 | ||
|
|
3d3fcf8fcb | ||
|
|
d99e3e52f3 | ||
|
|
bbcfc0ce59 | ||
|
|
d9ac6867cb | ||
|
|
00712b184b | ||
|
|
30ca617960 | ||
|
|
aba5205896 | ||
|
|
aef59d9281 | ||
|
|
b1582b3012 | ||
|
|
dd769d87c0 | ||
|
|
febe9a2882 | ||
|
|
337ccd7c62 | ||
|
|
c9789b3c18 | ||
|
|
c9db487613 | ||
|
|
77fffb4dc7 | ||
|
|
8701ec0968 | ||
|
|
94f3302aca | ||
|
|
16909a2b6b | ||
|
|
51fdd885ea | ||
|
|
a213f5a423 | ||
|
|
3a812a8b28 | ||
|
|
4375699013 | ||
|
|
53a6bbfdf8 | ||
|
|
897f1b97e3 | ||
|
|
309f1898b3 | ||
|
|
8998526384 | ||
|
|
e55e2a4274 | ||
|
|
29ec5d2898 | ||
|
|
adef9693af | ||
|
|
8f01ac42a8 | ||
|
|
8223a5235f | ||
|
|
fe5f2bd5d7 | ||
|
|
00075ac4ee | ||
|
|
3f39946f99 | ||
|
|
1ddfd55e51 | ||
|
|
5bb012b67b | ||
|
|
78fb987bef | ||
|
|
a0084dc223 | ||
|
|
f5ffbb4e00 | ||
|
|
13d2b0b558 | ||
|
|
b83b2bae3b | ||
|
|
ef84c16f37 | ||
|
|
47391fea3b | ||
|
|
afce8bc320 | ||
|
|
5d18cd3416 | ||
|
|
65e0b3b86f | ||
|
|
aa3171cf4b | ||
|
|
1754ac53cd | ||
|
|
5c6af65e48 | ||
|
|
d8871f56ba | ||
|
|
4d6bc3b5df | ||
|
|
60e253b387 | ||
|
|
127d6972ac | ||
|
|
6d7d22f3e6 | ||
|
|
6a4757ad06 | ||
|
|
ff0632c01e | ||
|
|
134501bf99 | ||
|
|
faa3943a25 | ||
|
|
b30f4ca12a | ||
|
|
208515dc38 | ||
|
|
9de3e80a4f | ||
|
|
63d8d0c5ac | ||
|
|
6b485c4e46 | ||
|
|
723e56ac50 | ||
|
|
0a4f7e0958 | ||
|
|
9eb6796bad | ||
|
|
1916f5be4b | ||
|
|
4207cb8450 | ||
|
|
231bfcf4cf | ||
|
|
03ceeb7211 | ||
|
|
118a322aa4 | ||
|
|
454ad7a1b4 | ||
|
|
266bceaffd | ||
|
|
60322ed491 | ||
|
|
18f4c1f646 | ||
|
|
45e6491a8e | ||
|
|
0108d5777c | ||
|
|
4fabb459aa | ||
|
|
75623173d4 | ||
|
|
f26f84cc4f | ||
|
|
d68d0b67ca | ||
|
|
e2ddf2ba52 | ||
|
|
21c06e86db | ||
|
|
93eaea9754 | ||
|
|
d1d8be8d9b | ||
|
|
b9b0b73d29 | ||
|
|
6dfd7fb518 | ||
|
|
fde196c86e | ||
|
|
6c08fae64f | ||
|
|
cdaf83247c | ||
|
|
c1911a00de | ||
|
|
56e3568492 | ||
|
|
19d3c44391 | ||
|
|
ab15ffcf3a |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -6,4 +6,4 @@ Please provide a brief description of the changes you made. Be as specific as po
|
||||
|
||||
The following checks are **mandatory**:
|
||||
|
||||
- [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).
|
||||
- [ ] My change adheres to [VictoriaMetrics contributing guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
|
||||
|
||||
41
Makefile
41
Makefile
@@ -5,7 +5,6 @@ MAKE_PARALLEL := $(MAKE) -j $(MAKE_CONCURRENCY)
|
||||
DATEINFO_TAG ?= $(shell date -u +'%Y%m%d-%H%M%S')
|
||||
BUILDINFO_TAG ?= $(shell echo $$(git describe --long --all | tr '/' '-')$$( \
|
||||
git diff-index --quiet HEAD -- || echo '-dirty-'$$(git diff-index -u HEAD | openssl sha1 | cut -d' ' -f2 | cut -c 1-8)))
|
||||
LATEST_TAG ?= latest
|
||||
|
||||
PKG_TAG ?= $(shell git tag -l --points-at HEAD)
|
||||
ifeq ($(PKG_TAG),)
|
||||
@@ -196,12 +195,31 @@ vmutils-crossbuild: \
|
||||
vmutils-openbsd-amd64 \
|
||||
vmutils-windows-amd64
|
||||
|
||||
publish-latest:
|
||||
PKG_TAG=$(TAG) APP_NAME=victoria-metrics $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmagent $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmalert $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmalert-tool $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmauth $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmbackup $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmrestore $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG) APP_NAME=vmctl $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG)-cluster APP_NAME=vminsert $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG)-cluster APP_NAME=vmselect $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG)-cluster APP_NAME=vmstorage $(MAKE) publish-via-docker-latest && \
|
||||
PKG_TAG=$(TAG)-enterprise APP_NAME=vmgateway $(MAKE) publish-via-docker-latest
|
||||
PKG_TAG=$(TAG)-enterprise APP_NAME=vmbackupmanager $(MAKE) publish-via-docker-latest
|
||||
|
||||
publish-victoria-logs-latest:
|
||||
PKG_TAG=$(TAG) APP_NAME=victoria-logs $(MAKE) publish-via-docker-latest
|
||||
PKG_TAG=$(TAG) APP_NAME=vlogscli $(MAKE) publish-via-docker-latest
|
||||
|
||||
publish-release:
|
||||
rm -rf bin/*
|
||||
git checkout $(TAG) && $(MAKE) release && LATEST_TAG=stable $(MAKE) publish && \
|
||||
git checkout $(TAG)-cluster && $(MAKE) release && LATEST_TAG=cluster-stable $(MAKE) publish && \
|
||||
git checkout $(TAG)-enterprise && $(MAKE) release && LATEST_TAG=enterprise-stable $(MAKE) publish && \
|
||||
git checkout $(TAG)-enterprise-cluster && $(MAKE) release && LATEST_TAG=enterprise-cluster-stable $(MAKE) publish
|
||||
git checkout $(TAG) && $(MAKE) release && $(MAKE) publish && \
|
||||
git checkout $(TAG)-cluster && $(MAKE) release && $(MAKE) publish && \
|
||||
git checkout $(TAG)-enterprise && $(MAKE) release && $(MAKE) publish && \
|
||||
git checkout $(TAG)-enterprise-cluster && $(MAKE) release && $(MAKE) publish
|
||||
|
||||
release:
|
||||
$(MAKE_PARALLEL) \
|
||||
@@ -510,8 +528,6 @@ vet:
|
||||
|
||||
check-all: fmt vet golangci-lint govulncheck
|
||||
|
||||
clean-checkers: remove-golangci-lint remove-govulncheck
|
||||
|
||||
test:
|
||||
GOEXPERIMENT=synctest go test ./lib/... ./app/...
|
||||
|
||||
@@ -527,7 +543,7 @@ test-full:
|
||||
test-full-386:
|
||||
GOEXPERIMENT=synctest GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
|
||||
integration-test: victoria-metrics vmagent vmalert vmauth
|
||||
integration-test: victoria-metrics vmagent vmalert vmauth vmctl
|
||||
go test ./apptest/... -skip="^TestCluster.*"
|
||||
|
||||
benchmark:
|
||||
@@ -556,12 +572,11 @@ app-local-goos-goarch:
|
||||
app-local-windows-goarch:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
|
||||
|
||||
quicktemplate-gen: install-qtc
|
||||
qtc
|
||||
|
||||
install-qtc:
|
||||
which qtc || go install github.com/valyala/quicktemplate/qtc@latest
|
||||
quicktemplate-gen:
|
||||
go tool qtc
|
||||
|
||||
golangci-lint:
|
||||
GOEXPERIMENT=synctest go tool golangci-lint run
|
||||
|
||||
golangci-lint: install-golangci-lint
|
||||
GOEXPERIMENT=synctest golangci-lint run
|
||||
|
||||
12
README.md
12
README.md
@@ -40,16 +40,16 @@ 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](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter), [Prometheus remote write API](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus), [Prometheus exposition format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-prometheus-exposition-format).
|
||||
* [InfluxDB line protocol](https://docs.victoriametrics.com/victoriametrics/integrations/influxdb) over HTTP, TCP and UDP.
|
||||
* [Prometheus exporters](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter), [Prometheus remote write API](https://docs.victoriametrics.com/victoriametrics/integrations/prometheus/), [Prometheus exposition format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-prometheus-exposition-format).
|
||||
* [InfluxDB line protocol](https://docs.victoriametrics.com/victoriametrics/integrations/influxdb/) over HTTP, TCP and UDP.
|
||||
* [Graphite plaintext protocol](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#ingesting) with [tags](https://graphite.readthedocs.io/en/latest/tags.html#carbon).
|
||||
* [OpenTSDB put message](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb#sending-data-via-telnet).
|
||||
* [HTTP OpenTSDB /api/put requests](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb#sending-data-via-http).
|
||||
* [OpenTSDB put message](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb/#sending-data-via-telnet).
|
||||
* [HTTP OpenTSDB /api/put requests](https://docs.victoriametrics.com/victoriametrics/integrations/opentsdb/#sending-data-via-http).
|
||||
* [JSON line format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-json-line-format).
|
||||
* [Arbitrary CSV data](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-csv-data).
|
||||
* [Native binary format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-import-data-in-native-format).
|
||||
* [DataDog agent or DogStatsD](https://docs.victoriametrics.com/victoriametrics/integrations/datadog).
|
||||
* [NewRelic infrastructure agent](https://docs.victoriametrics.com/victoriametrics/integrations/newrelic#sending-data-from-agent).
|
||||
* [DataDog agent or DogStatsD](https://docs.victoriametrics.com/victoriametrics/integrations/datadog/).
|
||||
* [NewRelic infrastructure agent](https://docs.victoriametrics.com/victoriametrics/integrations/newrelic/#sending-data-from-agent).
|
||||
* [OpenTelemetry metrics format](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#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,6 +95,7 @@ func datadogLogsIngestion(w http.ResponseWriter, r *http.Request) bool {
|
||||
// There is no need in updating v2LogsRequestDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
v2LogsRequestDuration.UpdateDuration(startTime)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
fmt.Fprintf(w, `{}`)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -101,9 +101,11 @@ func (lr *LineReader) readMoreData() bool {
|
||||
|
||||
bufLen := len(lr.buf)
|
||||
if bufLen >= MaxLineSizeBytes.IntN() {
|
||||
logger.Warnf("%s: the line length exceeds -insert.maxLineSizeBytes=%d; skipping it; line contents=%q", lr.name, MaxLineSizeBytes.IntN(), lr.buf)
|
||||
ok, skippedBytes := lr.skipUntilNextLine()
|
||||
logger.Warnf("%s: the line length exceeds -insert.maxLineSizeBytes=%d; skipping it; total skipped bytes=%d",
|
||||
lr.name, MaxLineSizeBytes.IntN(), skippedBytes)
|
||||
tooLongLinesSkipped.Inc()
|
||||
return lr.skipUntilNextLine()
|
||||
return ok
|
||||
}
|
||||
|
||||
lr.buf = slicesutil.SetLength(lr.buf, MaxLineSizeBytes.IntN())
|
||||
@@ -121,26 +123,35 @@ func (lr *LineReader) readMoreData() bool {
|
||||
|
||||
var tooLongLinesSkipped = metrics.NewCounter("vl_too_long_lines_skipped_total")
|
||||
|
||||
func (lr *LineReader) skipUntilNextLine() bool {
|
||||
func (lr *LineReader) skipUntilNextLine() (bool, int) {
|
||||
|
||||
// Initialize skipped bytes count with MaxLineSizeBytes because
|
||||
// we've already read that many bytes without encountering a newline,
|
||||
// indicating the line size exceeds the maximum allowed limit.
|
||||
skipSizeBytes := MaxLineSizeBytes.IntN()
|
||||
|
||||
for {
|
||||
lr.buf = slicesutil.SetLength(lr.buf, MaxLineSizeBytes.IntN())
|
||||
n, err := lr.r.Read(lr.buf)
|
||||
skipSizeBytes += n
|
||||
lr.buf = lr.buf[:n]
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
lr.eofReached = true
|
||||
lr.buf = lr.buf[:0]
|
||||
return true
|
||||
return true, skipSizeBytes
|
||||
}
|
||||
lr.err = fmt.Errorf("cannot skip the current line: %s", err)
|
||||
return false
|
||||
return false, skipSizeBytes
|
||||
}
|
||||
if n := bytes.IndexByte(lr.buf, '\n'); n >= 0 {
|
||||
// Include skipped bytes before \n, including the newline itself.
|
||||
skipSizeBytes += n + 1 - len(lr.buf)
|
||||
// Include \n in the buf, so too long line is replaced with an empty line.
|
||||
// This is needed for maintaining synchorinzation consistency between lines
|
||||
// in protocols such as Elasticsearch bulk import.
|
||||
lr.buf = append(lr.buf[:0], lr.buf[n:]...)
|
||||
return true
|
||||
return true, skipSizeBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package internalinsert
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -18,17 +17,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
disableInsert = flag.Bool("internalinsert.disable", false, "Whether to disable /internal/insert HTTP endpoint")
|
||||
maxRequestSize = flagutil.NewBytes("internalinsert.maxRequestSize", 64*1024*1024, "The maximum size in bytes of a single request, which can be accepted at /internal/insert HTTP endpoint")
|
||||
)
|
||||
|
||||
// RequestHandler processes /internal/insert requests.
|
||||
func RequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if *disableInsert {
|
||||
httpserver.Errorf(w, r, "requests to /internal/insert are disabled with -internalinsert.disable command-line flag")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vlinsert
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -13,6 +14,12 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/loki"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/opentelemetry"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/syslog"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
|
||||
)
|
||||
|
||||
var (
|
||||
disableInsert = flag.Bool("insert.disable", false, "Whether to disable /insert/* HTTP endpoints")
|
||||
disableInternal = flag.Bool("internalinsert.disable", false, "Whether to disable /internal/insert HTTP endpoint")
|
||||
)
|
||||
|
||||
// Init initializes vlinsert
|
||||
@@ -27,19 +34,31 @@ func Stop() {
|
||||
|
||||
// RequestHandler handles insert requests for VictoriaLogs
|
||||
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
path := strings.ReplaceAll(r.URL.Path, "//", "/")
|
||||
|
||||
if strings.HasPrefix(path, "/insert/") {
|
||||
if *disableInsert {
|
||||
httpserver.Errorf(w, r, "requests to /insert/* are disabled with -insert.disable command-line flag")
|
||||
return true
|
||||
}
|
||||
|
||||
return insertHandler(w, r, path)
|
||||
}
|
||||
|
||||
if path == "/internal/insert" {
|
||||
if *disableInternal || *disableInsert {
|
||||
httpserver.Errorf(w, r, "requests to /internal/insert are disabled with -internalinsert.disable or -insert.disable command-line flag")
|
||||
return true
|
||||
}
|
||||
internalinsert.RequestHandler(w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "/insert/") {
|
||||
// Skip requests, which do not start with /insert/, since these aren't our requests.
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func insertHandler(w http.ResponseWriter, r *http.Request, path string) bool {
|
||||
path = strings.TrimPrefix(path, "/insert")
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
|
||||
switch path {
|
||||
case "/jsonline":
|
||||
@@ -69,7 +88,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
case strings.HasPrefix(path, "/datadog/"):
|
||||
path = strings.TrimPrefix(path, "/datadog")
|
||||
return datadog.RequestHandler(path, w, r)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/pb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/slicesutil"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
@@ -53,7 +52,7 @@ func handleProtobuf(r *http.Request, w http.ResponseWriter) {
|
||||
err = protoparserutil.ReadUncompressedData(r.Body, encoding, maxRequestSize, func(data []byte) error {
|
||||
lmp := cp.NewLogMessageProcessor("opentelelemtry_protobuf", false)
|
||||
useDefaultStreamFields := len(cp.StreamFields) == 0
|
||||
err := pushProtobufRequest(data, lmp, useDefaultStreamFields)
|
||||
err := pushProtobufRequest(data, lmp, cp.MsgFields, useDefaultStreamFields)
|
||||
lmp.MustClose()
|
||||
return err
|
||||
})
|
||||
@@ -75,7 +74,7 @@ var (
|
||||
requestProtobufDuration = metrics.NewHistogram(`vl_http_request_duration_seconds{path="/insert/opentelemetry/v1/logs",format="protobuf"}`)
|
||||
)
|
||||
|
||||
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, useDefaultStreamFields bool) error {
|
||||
func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) error {
|
||||
var req pb.ExportLogsServiceRequest
|
||||
if err := req.UnmarshalProtobuf(data); err != nil {
|
||||
errorsTotal.Inc()
|
||||
@@ -84,35 +83,31 @@ func pushProtobufRequest(data []byte, lmp insertutil.LogMessageProcessor, useDef
|
||||
|
||||
var commonFields []logstorage.Field
|
||||
for _, rl := range req.ResourceLogs {
|
||||
attributes := rl.Resource.Attributes
|
||||
commonFields = slicesutil.SetLength(commonFields, len(attributes))
|
||||
for i, attr := range attributes {
|
||||
commonFields[i].Name = attr.Key
|
||||
commonFields[i].Value = attr.Value.FormatString(true)
|
||||
}
|
||||
commonFields = commonFields[:0]
|
||||
commonFields = appendKeyValues(commonFields, rl.Resource.Attributes, "")
|
||||
commonFieldsLen := len(commonFields)
|
||||
for _, sc := range rl.ScopeLogs {
|
||||
commonFields = pushFieldsFromScopeLogs(&sc, commonFields[:commonFieldsLen], lmp, useDefaultStreamFields)
|
||||
commonFields = pushFieldsFromScopeLogs(&sc, commonFields[:commonFieldsLen], lmp, msgFields, useDefaultStreamFields)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor, useDefaultStreamFields bool) []logstorage.Field {
|
||||
func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field, lmp insertutil.LogMessageProcessor, msgFields []string, useDefaultStreamFields bool) []logstorage.Field {
|
||||
fields := commonFields
|
||||
for _, lr := range sc.LogRecords {
|
||||
fields = fields[:len(commonFields)]
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "_msg",
|
||||
Value: lr.Body.FormatString(true),
|
||||
})
|
||||
for _, attr := range lr.Attributes {
|
||||
if lr.Body.KeyValueList != nil {
|
||||
fields = appendKeyValues(fields, lr.Body.KeyValueList.Values, "")
|
||||
logstorage.RenameField(fields[len(commonFields):], msgFields, "_msg")
|
||||
} else {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: attr.Key,
|
||||
Value: attr.Value.FormatString(true),
|
||||
Name: "_msg",
|
||||
Value: lr.Body.FormatString(true),
|
||||
})
|
||||
}
|
||||
fields = appendKeyValues(fields, lr.Attributes, "")
|
||||
if len(lr.TraceID) > 0 {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: "trace_id",
|
||||
@@ -138,3 +133,22 @@ func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field,
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func appendKeyValues(fields []logstorage.Field, kvs []*pb.KeyValue, parentField string) []logstorage.Field {
|
||||
for _, attr := range kvs {
|
||||
fieldName := attr.Key
|
||||
if parentField != "" {
|
||||
fieldName = parentField + "." + fieldName
|
||||
}
|
||||
|
||||
if attr.Value.KeyValueList != nil {
|
||||
fields = appendKeyValues(fields, attr.Value.KeyValueList.Values, fieldName)
|
||||
} else {
|
||||
fields = append(fields, logstorage.Field{
|
||||
Name: fieldName,
|
||||
Value: attr.Value.FormatString(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestPushProtoOk(t *testing.T) {
|
||||
|
||||
pData := lr.MarshalProtobuf(nil)
|
||||
tlp := &insertutil.TestLogMessageProcessor{}
|
||||
if err := pushProtobufRequest(pData, tlp, false); err != nil {
|
||||
if err := pushProtobufRequest(pData, tlp, nil, false); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ func TestPushProtoOk(t *testing.T) {
|
||||
},
|
||||
},
|
||||
[]int64{1234, 1235, 1236},
|
||||
`{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message","severity":"Trace"}
|
||||
{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message-msg-2","severity":"Unspecified"}
|
||||
{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message-msg-2","severity":"Unspecified"}`,
|
||||
`{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message","severity":"Trace"}
|
||||
{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message-msg-2","severity":"Unspecified"}
|
||||
{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message-msg-2","severity":"Unspecified"}`,
|
||||
)
|
||||
|
||||
// multi-scope with resource attributes and multi-line
|
||||
@@ -136,14 +136,71 @@ func TestPushProtoOk(t *testing.T) {
|
||||
},
|
||||
},
|
||||
[]int64{1234, 1235, 2345, 2346, 2347, 2348, 3333},
|
||||
`{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message","severity":"Trace"}
|
||||
{"logger":"context","instance_id":"10","node_taints":"{\"role\":\"dev\",\"cluster_load_percent\":0.55}","_msg":"log-line-message-msg-2","severity":"Debug"}
|
||||
`{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message","severity":"Trace"}
|
||||
{"logger":"context","instance_id":"10","node_taints.role":"dev","node_taints.cluster_load_percent":"0.55","_msg":"log-line-message-msg-2","severity":"Debug"}
|
||||
{"_msg":"log-line-resource-scope-1-0-0","severity":"Info2"}
|
||||
{"_msg":"log-line-resource-scope-1-0-1","severity":"Info2"}
|
||||
{"_msg":"log-line-resource-scope-1-1-0","severity":"Info4"}
|
||||
{"_msg":"log-line-resource-scope-1-1-1","trace_id":"1234","span_id":"45","severity":"Info4"}
|
||||
{"_msg":"log-line-resource-scope-1-1-2","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","severity":"Unspecified"}`,
|
||||
)
|
||||
|
||||
// nested fields
|
||||
f([]pb.ResourceLogs{
|
||||
{
|
||||
ScopeLogs: []pb.ScopeLogs{
|
||||
{
|
||||
LogRecords: []pb.LogRecord{
|
||||
{
|
||||
TimeUnixNano: 1234,
|
||||
Body: pb.AnyValue{StringValue: ptrTo("nested fields")},
|
||||
Attributes: []*pb.KeyValue{
|
||||
{Key: "error", Value: &pb.AnyValue{KeyValueList: &pb.KeyValueList{Values: []*pb.KeyValue{
|
||||
{
|
||||
Key: "type",
|
||||
Value: &pb.AnyValue{StringValue: ptrTo("document_parsing_exception")},
|
||||
},
|
||||
{
|
||||
Key: "reason",
|
||||
Value: &pb.AnyValue{StringValue: ptrTo("failed to parse field [_msg] of type [text]")},
|
||||
},
|
||||
{
|
||||
Key: "caused_by",
|
||||
Value: &pb.AnyValue{KeyValueList: &pb.KeyValueList{Values: []*pb.KeyValue{
|
||||
{
|
||||
Key: "type",
|
||||
Value: &pb.AnyValue{StringValue: ptrTo("x_content_parse_exception")},
|
||||
},
|
||||
{
|
||||
Key: "reason",
|
||||
Value: &pb.AnyValue{StringValue: ptrTo("unexpected end-of-input in VALUE_STRING")},
|
||||
},
|
||||
{
|
||||
Key: "caused_by",
|
||||
Value: &pb.AnyValue{KeyValueList: &pb.KeyValueList{Values: []*pb.KeyValue{
|
||||
{
|
||||
Key: "type",
|
||||
Value: &pb.AnyValue{StringValue: ptrTo("json_e_o_f_exception")},
|
||||
},
|
||||
{
|
||||
Key: "reason",
|
||||
Value: &pb.AnyValue{StringValue: ptrTo("eof")},
|
||||
},
|
||||
}}},
|
||||
},
|
||||
}}},
|
||||
},
|
||||
}}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []int64{1234},
|
||||
`{"_msg":"nested fields","error.type":"document_parsing_exception","error.reason":"failed to parse field [_msg] of type [text]",`+
|
||||
`"error.caused_by.type":"x_content_parse_exception","error.caused_by.reason":"unexpected end-of-input in VALUE_STRING",`+
|
||||
`"error.caused_by.caused_by.type":"json_e_o_f_exception","error.caused_by.caused_by.reason":"eof","severity":"Unspecified"}`)
|
||||
}
|
||||
|
||||
func ptrTo[T any](s T) *T {
|
||||
|
||||
@@ -27,7 +27,7 @@ func benchmarkParseProtobufRequest(b *testing.B, streams, rows, labels int) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
body := getProtobufBody(streams, rows, labels)
|
||||
for pb.Next() {
|
||||
if err := pushProtobufRequest(body, blp, false); err != nil {
|
||||
if err := pushProtobufRequest(body, blp, nil, false); err != nil {
|
||||
panic(fmt.Errorf("unexpected error: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ func generateAndPushLogs(cfg *workerConfig, workerID int) {
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot perform request to %q: %s", cfg.url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
logger.Fatalf("unexpected status code got from %q: %d; want 2xx", cfg.url, err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package internalselect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -22,15 +21,8 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
)
|
||||
|
||||
var disableSelect = flag.Bool("internalselect.disable", false, "Whether to disable /internal/select/* HTTP endpoints")
|
||||
|
||||
// RequestHandler processes requests to /internal/select/*
|
||||
func RequestHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
if *disableSelect {
|
||||
httpserver.Errorf(w, r, "requests to /internal/select/* are disabled with -internalselect.disable command-line flag")
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
path := r.URL.Path
|
||||
|
||||
@@ -55,7 +55,10 @@ func ProcessFacetsRequest(ctx context.Context, w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
keepConstFields := httputil.GetBool(r, "keep_const_fields")
|
||||
|
||||
// Pipes must be dropped, since it is expected facets are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
q.AddFacetsPipe(limit, maxValuesPerField, maxValueLen, keepConstFields)
|
||||
|
||||
var mLock sync.Mutex
|
||||
@@ -156,8 +159,10 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
fieldsLimit = 0
|
||||
}
|
||||
|
||||
// Prepare the query for hits count.
|
||||
// Pipes must be dropped, since it is expected hits are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
q.AddCountByTimePipe(int64(step), int64(offset), fields)
|
||||
|
||||
var mLock sync.Mutex
|
||||
@@ -290,6 +295,10 @@ func ProcessFieldNamesRequest(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Pipes must be dropped, since it is expected field names are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
// Obtain field names for the given query
|
||||
fieldNames, err := vlstorage.GetFieldNames(ctx, tenantIDs, q)
|
||||
if err != nil {
|
||||
@@ -329,6 +338,10 @@ func ProcessFieldValuesRequest(ctx context.Context, w http.ResponseWriter, r *ht
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Pipes must be dropped, since it is expected field values are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
// Obtain unique values for the given field
|
||||
values, err := vlstorage.GetFieldValues(ctx, tenantIDs, q, fieldName, uint64(limit))
|
||||
if err != nil {
|
||||
@@ -351,6 +364,10 @@ func ProcessStreamFieldNamesRequest(ctx context.Context, w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Pipes must be dropped, since it is expected stream field names are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
// Obtain stream field names for the given query
|
||||
names, err := vlstorage.GetStreamFieldNames(ctx, tenantIDs, q)
|
||||
if err != nil {
|
||||
@@ -389,6 +406,10 @@ func ProcessStreamFieldValuesRequest(ctx context.Context, w http.ResponseWriter,
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Pipes must be dropped, since it is expected stream field values are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
// Obtain stream field values for the given query and the given fieldName
|
||||
values, err := vlstorage.GetStreamFieldValues(ctx, tenantIDs, q, fieldName, uint64(limit))
|
||||
if err != nil {
|
||||
@@ -420,6 +441,10 @@ func ProcessStreamIDsRequest(ctx context.Context, w http.ResponseWriter, r *http
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Pipes must be dropped, since it is expected stream ids are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
// Obtain streamIDs for the given query
|
||||
streamIDs, err := vlstorage.GetStreamIDs(ctx, tenantIDs, q, uint64(limit))
|
||||
if err != nil {
|
||||
@@ -451,6 +476,10 @@ func ProcessStreamsRequest(ctx context.Context, w http.ResponseWriter, r *http.R
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Pipes must be dropped, since it is expected stream are obtained
|
||||
// from the real logs stored in the database.
|
||||
q.DropAllPipes()
|
||||
|
||||
// Obtain streams for the given query
|
||||
streams, err := vlstorage.GetStreams(ctx, tenantIDs, q, uint64(limit))
|
||||
if err != nil {
|
||||
@@ -514,6 +543,11 @@ 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)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-ndjson")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
flusher.Flush()
|
||||
|
||||
qOrig := q
|
||||
for {
|
||||
q = qOrig.CloneWithTimeFilter(end, start, end)
|
||||
@@ -546,7 +580,7 @@ var liveTailRequests = metrics.NewCounter(`vl_live_tailing_requests`)
|
||||
const tailOffsetNsecs = 5e9
|
||||
|
||||
type logRow struct {
|
||||
timestamp string
|
||||
timestamp int64
|
||||
fields []logstorage.Field
|
||||
}
|
||||
|
||||
@@ -562,7 +596,7 @@ type tailProcessor struct {
|
||||
mu sync.Mutex
|
||||
|
||||
perStreamRows map[string][]logRow
|
||||
lastTimestamps map[string]string
|
||||
lastTimestamps map[string]int64
|
||||
|
||||
err error
|
||||
}
|
||||
@@ -572,7 +606,7 @@ func newTailProcessor(cancel func()) *tailProcessor {
|
||||
cancel: cancel,
|
||||
|
||||
perStreamRows: make(map[string][]logRow),
|
||||
lastTimestamps: make(map[string]string),
|
||||
lastTimestamps: make(map[string]int64),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +623,7 @@ func (tp *tailProcessor) writeBlock(_ uint, db *logstorage.DataBlock) {
|
||||
}
|
||||
|
||||
// Make sure columns contain _time field, since it is needed for proper tail work.
|
||||
timestamps, ok := db.GetTimestamps()
|
||||
timestamps, ok := db.GetTimestamps(nil)
|
||||
if !ok {
|
||||
tp.err = fmt.Errorf("missing _time field")
|
||||
tp.cancel()
|
||||
@@ -1038,9 +1072,7 @@ func getLastNQueryResults(ctx context.Context, tenantIDs []logstorage.TenantID,
|
||||
}
|
||||
|
||||
func getLastNRows(rows []logRow, limit int) []logRow {
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].timestamp < rows[j].timestamp
|
||||
})
|
||||
sortLogRows(rows)
|
||||
if len(rows) > limit {
|
||||
rows = rows[len(rows)-limit:]
|
||||
}
|
||||
@@ -1065,7 +1097,7 @@ func getQueryResultsWithLimit(ctx context.Context, tenantIDs []logstorage.Tenant
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
|
||||
timestamps, ok := db.GetTimestamps()
|
||||
timestamps, ok := db.GetTimestamps(nil)
|
||||
if !ok {
|
||||
missingTimeColumn.Store(true)
|
||||
cancel()
|
||||
|
||||
@@ -25,6 +25,9 @@ var (
|
||||
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the search request waits for execution when -search.maxConcurrentRequests "+
|
||||
"limit is reached; see also -search.maxQueryDuration")
|
||||
maxQueryDuration = flag.Duration("search.maxQueryDuration", time.Second*30, "The maximum duration for query execution. It can be overridden to a smaller value on a per-query basis via 'timeout' query arg")
|
||||
|
||||
disableSelect = flag.Bool("select.disable", false, "Whether to disable /select/* HTTP endpoints")
|
||||
disableInternal = flag.Bool("internalselect.disable", false, "Whether to disable /internal/select/* HTTP endpoints")
|
||||
)
|
||||
|
||||
func getDefaultMaxConcurrentRequests() int {
|
||||
@@ -71,13 +74,31 @@ var vmuiFileServer = http.FileServer(http.FS(vmuiFiles))
|
||||
|
||||
// RequestHandler handles select requests for VictoriaLogs
|
||||
func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
path := r.URL.Path
|
||||
path := strings.ReplaceAll(r.URL.Path, "//", "/")
|
||||
|
||||
if !strings.HasPrefix(path, "/select/") && !strings.HasPrefix(path, "/internal/select/") {
|
||||
// Skip requests, which do not start with /select/, since these aren't our requests.
|
||||
return false
|
||||
if strings.HasPrefix(path, "/select/") {
|
||||
if *disableSelect {
|
||||
httpserver.Errorf(w, r, "requests to /select/* are disabled with -select.disable command-line flag")
|
||||
return true
|
||||
}
|
||||
|
||||
return selectHandler(w, r, path)
|
||||
}
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
|
||||
if strings.HasPrefix(path, "/internal/select/") {
|
||||
if *disableInternal || *disableSelect {
|
||||
httpserver.Errorf(w, r, "requests to /internal/select/* are disabled with -internalselect.disable or -select.disable command-line flag")
|
||||
return true
|
||||
}
|
||||
internalselect.RequestHandler(r.Context(), w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func selectHandler(w http.ResponseWriter, r *http.Request, path string) bool {
|
||||
ctx := r.Context()
|
||||
|
||||
if path == "/select/vmui" {
|
||||
// VMUI access via incomplete url without `/` in the end. Redirect to complete url.
|
||||
@@ -100,7 +121,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
if path == "/select/logsql/tail" {
|
||||
logsqlTailRequests.Inc()
|
||||
// Process live tailing request without timeout, since it is OK to run live tailing requests for very long time.
|
||||
@@ -120,13 +140,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
defer decRequestConcurrency()
|
||||
|
||||
if strings.HasPrefix(path, "/internal/select/") {
|
||||
// Process internal request from vlselect without timeout (e.g. use ctx instead of ctxWithTimeout),
|
||||
// since the timeout must be controlled by the vlselect.
|
||||
internalselect.RequestHandler(ctx, w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
ok := processSelectRequest(ctxWithTimeout, w, r, path)
|
||||
if !ok {
|
||||
return false
|
||||
|
||||
@@ -66,8 +66,8 @@ or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/victo
|
||||
The list of MetricsQL features on top of PromQL:
|
||||
|
||||
* Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#selecting-graphite-metrics).
|
||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#graphite-api-usage) for details.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
||||
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
||||
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
||||
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
||||
208
app/vlselect/vmui/assets/index-BaRvaPfA.js
Normal file
208
app/vlselect/vmui/assets/index-BaRvaPfA.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/vlselect/vmui/assets/index-C85_NB5q.css
Normal file
1
app/vlselect/vmui/assets/index-C85_NB5q.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
67
app/vlselect/vmui/assets/vendor-D8IJGiEn.js
Normal file
67
app/vlselect/vmui/assets/vendor-D8IJGiEn.js
Normal file
File diff suppressed because one or more lines are too long
@@ -35,10 +35,10 @@
|
||||
<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 type="module" crossorigin src="./assets/index-DfcWONVQ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-C-vZmbyg.js">
|
||||
<script type="module" crossorigin src="./assets/index-BaRvaPfA.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Brup_hCI.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -248,6 +248,9 @@ func (sn *storageNode) executeRequestAt(ctx context.Context, path string, args u
|
||||
if err != nil {
|
||||
logger.Panicf("BUG: unexpected error when creating a request: %s", err)
|
||||
}
|
||||
if err := sn.ac.SetHeaders(req, true); err != nil {
|
||||
return nil, fmt.Errorf("cannot set auth headers for %q: %w", reqURL, err)
|
||||
}
|
||||
|
||||
// send the request to the storage node
|
||||
resp, err := sn.c.Do(req)
|
||||
|
||||
@@ -22,10 +22,10 @@ var (
|
||||
relabelConfigPathGlobal = flag.String("remoteWrite.relabelConfig", "", "Optional path to file with relabeling configs, which are applied "+
|
||||
"to all the metrics before sending them to -remoteWrite.url. See also -remoteWrite.urlRelabelConfig. "+
|
||||
"The path can point either to local file or to http url. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling")
|
||||
"See https://docs.victoriametrics.com/victoriametrics/relabeling/")
|
||||
relabelConfigPaths = flagutil.NewArrayString("remoteWrite.urlRelabelConfig", "Optional path to relabel configs for the corresponding -remoteWrite.url. "+
|
||||
"See also -remoteWrite.relabelConfig. The path can point either to local file or to http url. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling")
|
||||
"See https://docs.victoriametrics.com/victoriametrics/relabeling/")
|
||||
|
||||
usePromCompatibleNaming = flag.Bool("usePromCompatibleNaming", false, "Whether to replace characters unsupported by Prometheus with underscores "+
|
||||
"in the ingested metric names and label names. For example, foo.bar{a.b='c'} is transformed into foo_bar{a_b='c'} during data ingestion if this flag is set. "+
|
||||
@@ -121,7 +121,7 @@ type relabelConfigs struct {
|
||||
}
|
||||
|
||||
func (rcs *relabelConfigs) isSet() bool {
|
||||
if rcs != nil {
|
||||
if rcs == nil {
|
||||
return false
|
||||
}
|
||||
if rcs.global.Len() > 0 {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -99,8 +98,11 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
|
||||
}()
|
||||
}
|
||||
|
||||
// adding time.Now().UnixNano() to avoid possible file conflict when multiple processes run on a single host
|
||||
storagePath = filepath.Join(os.TempDir(), testStoragePath, strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||
tmpFolder, err := os.MkdirTemp(os.TempDir(), testStoragePath)
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to create tmp dir for tests: %v", err)
|
||||
}
|
||||
storagePath = tmpFolder
|
||||
processFlags()
|
||||
vminsert.Init()
|
||||
vmselect.Init()
|
||||
|
||||
@@ -258,12 +258,18 @@ func (rr *RecordingRule) toTimeSeries(m datasource.Metric) prompbmarshal.TimeSer
|
||||
Value: rr.Name,
|
||||
})
|
||||
}
|
||||
// add extra labels configured by user
|
||||
for k := range rr.Labels {
|
||||
prevLabel := promrelabel.GetLabelByName(m.Labels, k)
|
||||
if prevLabel != nil && prevLabel.Value != rr.Labels[k] {
|
||||
// Rename the prevLabel to "exported_" + label.Name
|
||||
prevLabel.Name = fmt.Sprintf("exported_%s", prevLabel.Name)
|
||||
existingLabel := promrelabel.GetLabelByName(m.Labels, k)
|
||||
if existingLabel != nil { // there is a conflict between extra and existing label
|
||||
if existingLabel.Value == rr.Labels[k] {
|
||||
// extra and existing labels are identical - do nothing
|
||||
continue
|
||||
}
|
||||
// preserve existing label by adding "exported_" prefix
|
||||
existingLabel.Name = fmt.Sprintf("exported_%s", existingLabel.Name)
|
||||
}
|
||||
// add extra label
|
||||
m.Labels = append(m.Labels, prompbmarshal.Label{
|
||||
Name: k,
|
||||
Value: rr.Labels[k],
|
||||
|
||||
@@ -168,6 +168,7 @@ func TestRecordingRule_Exec(t *testing.T) {
|
||||
}, [][]datasource.Metric{{
|
||||
metricWithValueAndLabels(t, 2, "__name__", "foo", "job", "foo"),
|
||||
metricWithValueAndLabels(t, 1, "__name__", "bar", "job", "bar", "source", "origin"),
|
||||
metricWithValueAndLabels(t, 1, "__name__", "baz", "job", "baz", "source", "test"),
|
||||
}}, [][]prompbmarshal.TimeSeries{{
|
||||
newTimeSeries([]float64{2}, []int64{ts.UnixNano()}, []prompbmarshal.Label{
|
||||
{
|
||||
@@ -202,6 +203,21 @@ func TestRecordingRule_Exec(t *testing.T) {
|
||||
Value: "origin",
|
||||
},
|
||||
}),
|
||||
newTimeSeries([]float64{1}, []int64{ts.UnixNano()},
|
||||
[]prompbmarshal.Label{
|
||||
{
|
||||
Name: "__name__",
|
||||
Value: "job:foo",
|
||||
},
|
||||
{
|
||||
Name: "job",
|
||||
Value: "baz",
|
||||
},
|
||||
{
|
||||
Name: "source",
|
||||
Value: "test",
|
||||
},
|
||||
}),
|
||||
}})
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,9 @@ func normalizeURL(uOrig *url.URL) *url.URL {
|
||||
u := *uOrig
|
||||
// Prevent from attacks with using `..` in r.URL.Path
|
||||
u.Path = path.Clean(u.Path)
|
||||
if u.Path == "." {
|
||||
u.Path = "/"
|
||||
}
|
||||
if !strings.HasSuffix(u.Path, "/") && strings.HasSuffix(uOrig.Path, "/") {
|
||||
// The path.Clean() removes trailing slash.
|
||||
// Return it back if needed.
|
||||
|
||||
@@ -128,7 +128,40 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
// Simple routing with `url_prefix`
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "", "http://foo.bar/.", "", "", nil, "least_loaded", 0)
|
||||
}, "", "http://foo.bar", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "/", "http://foo.bar", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "http://aaa///", "http://foo.bar", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/"),
|
||||
}, "/", "http://foo.bar/", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/"),
|
||||
}, "/x", "http://foo.bar/x", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/"),
|
||||
}, "/x/", "http://foo.bar/x/", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/"),
|
||||
}, "http://abc///x/", "http://foo.bar/x/", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/"),
|
||||
}, "http://foo//x", "http://foo.bar/x", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/baz"),
|
||||
}, "", "http://foo.bar/baz", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/baz"),
|
||||
}, "/", "http://foo.bar/baz", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/x/"),
|
||||
}, "/abc", "http://foo.bar/x/abc", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/x/"),
|
||||
}, "/abc/", "http://foo.bar/x/abc/", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
HeadersConf: HeadersConf{
|
||||
@@ -149,6 +182,12 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "a/b?c=d", "http://foo.bar/a/b?c=d", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar"),
|
||||
}, "/a/b?c=d", "http://foo.bar/a/b?c=d", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("http://foo.bar/"),
|
||||
}, "/a/b?c=d", "http://foo.bar/a/b?c=d", "", "", nil, "least_loaded", 0)
|
||||
f(&UserInfo{
|
||||
URLPrefix: mustParseURL("https://sss:3894/x/y"),
|
||||
}, "/z", "https://sss:3894/x/y/z", "", "", nil, "least_loaded", 0)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/native"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/remoteread"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/influx"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/opentsdb"
|
||||
@@ -44,6 +45,7 @@ func main() {
|
||||
if c.Bool(globalDisableProgressBar) {
|
||||
barpool.Disable(true)
|
||||
}
|
||||
netutil.EnableIPv6()
|
||||
return nil
|
||||
}
|
||||
app := &cli.App{
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/tsdb"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/backoff"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/barpool"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/prometheus"
|
||||
remote_read_integration "github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/testdata/servers_integration_test"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||
)
|
||||
|
||||
const (
|
||||
testSnapshot = "./testdata/snapshots/20250118T124506Z-59d1b952d7eaf547"
|
||||
blockData = "./testdata/snapshots/20250118T124506Z-59d1b952d7eaf547/01JHWQ445Y2P1TDYB05AEKD6MC"
|
||||
)
|
||||
|
||||
// This test simulates close process if user abort it
|
||||
func TestPrometheusProcessorRun(t *testing.T) {
|
||||
|
||||
f := func(startStr, endStr string, numOfSeries int, resultExpected []vm.TimeSeries) {
|
||||
t.Helper()
|
||||
|
||||
dst := remote_read_integration.NewRemoteWriteServer(t)
|
||||
|
||||
defer func() {
|
||||
dst.Close()
|
||||
}()
|
||||
|
||||
dst.Series(resultExpected)
|
||||
dst.ExpectedSeries(resultExpected)
|
||||
|
||||
if err := fillStorage(resultExpected); err != nil {
|
||||
t.Fatalf("cannot fill storage: %s", err)
|
||||
}
|
||||
|
||||
isSilent = true
|
||||
defer func() { isSilent = false }()
|
||||
|
||||
bf, err := backoff.New(1, 1.8, time.Second*2)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create backoff: %s", err)
|
||||
}
|
||||
|
||||
importerCfg := vm.Config{
|
||||
Addr: dst.URL(),
|
||||
Transport: nil,
|
||||
Concurrency: 1,
|
||||
Backoff: bf,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
importer, err := vm.NewImporter(ctx, importerCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create importer: %s", err)
|
||||
}
|
||||
defer importer.Close()
|
||||
|
||||
matchName := "__name__"
|
||||
matchValue := ".*"
|
||||
filter := prometheus.Filter{
|
||||
TimeMin: startStr,
|
||||
TimeMax: endStr,
|
||||
Label: matchName,
|
||||
LabelValue: matchValue,
|
||||
}
|
||||
|
||||
runner, err := prometheus.NewClient(prometheus.Config{
|
||||
Snapshot: testSnapshot,
|
||||
Filter: filter,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create prometheus client: %s", err)
|
||||
}
|
||||
p := &prometheusProcessor{
|
||||
cl: runner,
|
||||
im: importer,
|
||||
cc: 1,
|
||||
}
|
||||
|
||||
if err := p.run(); err != nil {
|
||||
t.Fatalf("run() error: %s", err)
|
||||
}
|
||||
|
||||
collectedTs := dst.GetCollectedTimeSeries()
|
||||
t.Logf("collected timeseries: %d; expected timeseries: %d", len(collectedTs), len(resultExpected))
|
||||
if len(collectedTs) != len(resultExpected) {
|
||||
t.Fatalf("unexpected number of collected time series; got %d; want %d", len(collectedTs), numOfSeries)
|
||||
}
|
||||
|
||||
deleted, err := deleteSeries(matchName, matchValue)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot delete series: %s", err)
|
||||
}
|
||||
if deleted != numOfSeries {
|
||||
t.Fatalf("unexpected number of deleted series; got %d; want %d", deleted, numOfSeries)
|
||||
}
|
||||
}
|
||||
|
||||
processFlags()
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
defer func() {
|
||||
vmstorage.Stop()
|
||||
if err := os.RemoveAll(storagePath); err != nil {
|
||||
log.Fatalf("cannot remove %q: %s", storagePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
barpool.Disable(true)
|
||||
defer func() {
|
||||
barpool.Disable(false)
|
||||
}()
|
||||
|
||||
b, err := tsdb.OpenBlock(nil, blockData, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open block: %s", err)
|
||||
}
|
||||
// timestamp is equal to minTime and maxTime from meta.json
|
||||
ss, err := readBlock(b, 1737204082361, 1737204302539)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot read block: %s", err)
|
||||
}
|
||||
|
||||
resultExpected, err := prepareExpectedData(ss)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot prepare expected data: %s", err)
|
||||
}
|
||||
|
||||
f("2025-01-18T12:40:00Z", "2025-01-18T12:46:00Z", 2792, resultExpected)
|
||||
}
|
||||
|
||||
func readBlock(b tsdb.BlockReader, timeMin int64, timeMax int64) (storage.SeriesSet, error) {
|
||||
minTime, maxTime := b.Meta().MinTime, b.Meta().MaxTime
|
||||
|
||||
if timeMin != 0 {
|
||||
minTime = timeMin
|
||||
}
|
||||
if timeMax != 0 {
|
||||
maxTime = timeMax
|
||||
}
|
||||
|
||||
q, err := tsdb.NewBlockQuerier(b, minTime, maxTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchName := "__name__"
|
||||
matchValue := ".*"
|
||||
ctx := context.Background()
|
||||
ss := q.Select(ctx, false, nil, labels.MustNewMatcher(labels.MatchRegexp, matchName, matchValue))
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func prepareExpectedData(ss storage.SeriesSet) ([]vm.TimeSeries, error) {
|
||||
var expectedSeriesSet []vm.TimeSeries
|
||||
var it chunkenc.Iterator
|
||||
for ss.Next() {
|
||||
var name string
|
||||
var labelPairs []vm.LabelPair
|
||||
series := ss.At()
|
||||
|
||||
for _, label := range series.Labels() {
|
||||
if label.Name == "__name__" {
|
||||
name = label.Value
|
||||
continue
|
||||
}
|
||||
labelPairs = append(labelPairs, vm.LabelPair{
|
||||
Name: label.Name,
|
||||
Value: label.Value,
|
||||
})
|
||||
}
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("failed to find `__name__` label in labelset for block")
|
||||
}
|
||||
|
||||
var timestamps []int64
|
||||
var values []float64
|
||||
it = series.Iterator(it)
|
||||
for {
|
||||
typ := it.Next()
|
||||
if typ == chunkenc.ValNone {
|
||||
break
|
||||
}
|
||||
if typ != chunkenc.ValFloat {
|
||||
// Skip unsupported values
|
||||
continue
|
||||
}
|
||||
t, v := it.At()
|
||||
timestamps = append(timestamps, t)
|
||||
values = append(values, v)
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := vm.TimeSeries{
|
||||
Name: name,
|
||||
LabelPairs: labelPairs,
|
||||
Timestamps: timestamps,
|
||||
Values: values,
|
||||
}
|
||||
expectedSeriesSet = append(expectedSeriesSet, ts)
|
||||
}
|
||||
return expectedSeriesSet, nil
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"ulid": "01JHWQ445Y2P1TDYB05AEKD6MC",
|
||||
"minTime": 1737204082361,
|
||||
"maxTime": 1737204302539,
|
||||
"stats": {
|
||||
"numSamples": 60275,
|
||||
"numSeries": 2792,
|
||||
"numChunks": 2792
|
||||
},
|
||||
"compaction": {
|
||||
"level": 1,
|
||||
"sources": [
|
||||
"01JHWQ445Y2P1TDYB05AEKD6MC"
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -299,6 +299,8 @@ func (im *Importer) Ping() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
var maxGraphiteSeries = flag.Int("search.maxGraphiteSeries", 300e3, "The maximum number of time series, which can be scanned during queries to Graphite Render API. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite#render-api")
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#render-api")
|
||||
|
||||
type evalConfig struct {
|
||||
startTime int64
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
|
||||
var (
|
||||
maxGraphiteTagKeysPerSearch = flag.Int("search.maxGraphiteTagKeys", 100e3, "The maximum number of tag keys returned from Graphite API, which returns tags. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite#tags-api")
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#tags-api")
|
||||
maxGraphiteTagValuesPerSearch = flag.Int("search.maxGraphiteTagValues", 100e3, "The maximum number of tag values returned from Graphite API, which returns tag values. "+
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite#tags-api")
|
||||
"See https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#tags-api")
|
||||
)
|
||||
|
||||
// TagsDelSeriesHandler implements /tags/delSeries handler.
|
||||
|
||||
@@ -32,9 +32,13 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries
|
||||
{% code
|
||||
// seriesFetched is string instead of int because of historical reasons.
|
||||
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
|
||||
executionDuration := int64(0)
|
||||
if ed := qs.ExecutionDuration.Load(); ed != nil {
|
||||
executionDuration = ed.Milliseconds()
|
||||
}
|
||||
%}
|
||||
"seriesFetched": "{%dl qs.SeriesFetched.Load() %}",
|
||||
"executionTimeMsec": {%dl qs.ExecutionDuration.Load().Milliseconds() %}
|
||||
"executionTimeMsec": {%dl executionDuration %}
|
||||
}
|
||||
{% code
|
||||
qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount)
|
||||
|
||||
@@ -64,91 +64,95 @@ func StreamQueryRangeResponse(qw422016 *qt422016.Writer, rs []netstorage.Result,
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:33
|
||||
// seriesFetched is string instead of int because of historical reasons.
|
||||
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
|
||||
executionDuration := int64(0)
|
||||
if ed := qs.ExecutionDuration.Load(); ed != nil {
|
||||
executionDuration = ed.Milliseconds()
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:35
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:39
|
||||
qw422016.N().S(`"seriesFetched": "`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:36
|
||||
qw422016.N().DL(qs.SeriesFetched.Load())
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:36
|
||||
qw422016.N().S(`","executionTimeMsec":`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:37
|
||||
qw422016.N().DL(qs.ExecutionDuration.Load().Milliseconds())
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:37
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:40
|
||||
qw422016.N().DL(qs.SeriesFetched.Load())
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:40
|
||||
qw422016.N().S(`","executionTimeMsec":`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:41
|
||||
qw422016.N().DL(executionDuration)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:41
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:44
|
||||
qt.Printf("generate /api/v1/query_range response for series=%d, points=%d", seriesCount, pointsCount)
|
||||
qtDone()
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:43
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:47
|
||||
streamdumpQueryTrace(qw422016, qt)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:43
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:47
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
func WriteQueryRangeResponse(qq422016 qtio422016.Writer, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) {
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
StreamQueryRangeResponse(qw422016, rs, qt, qtDone, qs)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
func QueryRangeResponse(rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string {
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
WriteQueryRangeResponse(qb422016, rs, qt, qtDone, qs)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:51
|
||||
func streamqueryRangeLine(qw422016 *qt422016.Writer, r *netstorage.Result) {
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:51
|
||||
qw422016.N().S(`{"metric":`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:53
|
||||
streammetricNameObject(qw422016, &r.MetricName)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:49
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:53
|
||||
qw422016.N().S(`,"values":`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:50
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:54
|
||||
streamvaluesWithTimestamps(qw422016, r.Values, r.Timestamps)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:50
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:54
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
func writequeryRangeLine(qq422016 qtio422016.Writer, r *netstorage.Result) {
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
streamqueryRangeLine(qw422016, r)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
func queryRangeLine(r *netstorage.Result) string {
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
writequeryRangeLine(qb422016, r)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:52
|
||||
//line app/vmselect/prometheus/query_range_response.qtpl:56
|
||||
}
|
||||
|
||||
@@ -34,9 +34,13 @@ See https://prometheus.io/docs/prometheus/latest/querying/api/#instant-queries
|
||||
{% code
|
||||
// seriesFetched is string instead of int because of historical reasons.
|
||||
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
|
||||
executionDuration := int64(0)
|
||||
if ed := qs.ExecutionDuration.Load(); ed != nil {
|
||||
executionDuration = ed.Milliseconds()
|
||||
}
|
||||
%}
|
||||
"seriesFetched": "{%dl qs.SeriesFetched.Load() %}",
|
||||
"executionTimeMsec": {%dl qs.ExecutionDuration.Load().Milliseconds() %}
|
||||
"executionTimeMsec": {%dl executionDuration %}
|
||||
}
|
||||
{% code
|
||||
qt.Printf("generate /api/v1/query response for series=%d", seriesCount)
|
||||
|
||||
@@ -74,50 +74,54 @@ func StreamQueryResponse(qw422016 *qt422016.Writer, rs []netstorage.Result, qt *
|
||||
//line app/vmselect/prometheus/query_response.qtpl:35
|
||||
// seriesFetched is string instead of int because of historical reasons.
|
||||
// It cannot be converted to int without breaking backwards compatibility at vmalert :(
|
||||
executionDuration := int64(0)
|
||||
if ed := qs.ExecutionDuration.Load(); ed != nil {
|
||||
executionDuration = ed.Milliseconds()
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_response.qtpl:37
|
||||
//line app/vmselect/prometheus/query_response.qtpl:41
|
||||
qw422016.N().S(`"seriesFetched": "`)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:38
|
||||
qw422016.N().DL(qs.SeriesFetched.Load())
|
||||
//line app/vmselect/prometheus/query_response.qtpl:38
|
||||
qw422016.N().S(`","executionTimeMsec":`)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:39
|
||||
qw422016.N().DL(qs.ExecutionDuration.Load().Milliseconds())
|
||||
//line app/vmselect/prometheus/query_response.qtpl:39
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:42
|
||||
qw422016.N().DL(qs.SeriesFetched.Load())
|
||||
//line app/vmselect/prometheus/query_response.qtpl:42
|
||||
qw422016.N().S(`","executionTimeMsec":`)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:43
|
||||
qw422016.N().DL(executionDuration)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:43
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:46
|
||||
qt.Printf("generate /api/v1/query response for series=%d", seriesCount)
|
||||
qtDone()
|
||||
|
||||
//line app/vmselect/prometheus/query_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_response.qtpl:49
|
||||
streamdumpQueryTrace(qw422016, qt)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:45
|
||||
//line app/vmselect/prometheus/query_response.qtpl:49
|
||||
qw422016.N().S(`}`)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
func WriteQueryResponse(qq422016 qtio422016.Writer, rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) {
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
StreamQueryResponse(qw422016, rs, qt, qtDone, qs)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
}
|
||||
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
func QueryResponse(rs []netstorage.Result, qt *querytracer.Tracer, qtDone func(), qs *promql.QueryStats) string {
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
WriteQueryResponse(qb422016, rs, qt, qtDone, qs)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
return qs422016
|
||||
//line app/vmselect/prometheus/query_response.qtpl:47
|
||||
//line app/vmselect/prometheus/query_response.qtpl:51
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/atomicutil"
|
||||
)
|
||||
|
||||
// callbacks for optimized incremental calculations for aggregate functions
|
||||
@@ -66,9 +68,8 @@ var incrementalAggrFuncCallbacksMap = map[string]*incrementalAggrFuncCallbacks{
|
||||
type incrementalAggrContextMap struct {
|
||||
m map[string]*incrementalAggrContext
|
||||
|
||||
// The padding prevents false sharing on widespread platforms with
|
||||
// 128 mod (cache line size) = 0 .
|
||||
_ [128 - unsafe.Sizeof(map[string]*incrementalAggrContext{})%128]byte
|
||||
// The padding prevents false sharing
|
||||
_ [atomicutil.CacheLineSize - unsafe.Sizeof(map[string]*incrementalAggrContext{})%atomicutil.CacheLineSize]byte
|
||||
}
|
||||
|
||||
type incrementalAggrFuncContext struct {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/atomicutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
@@ -1885,9 +1886,8 @@ func doRollupForTimeseries(funcName string, keepMetricNames bool, rc *rollupConf
|
||||
type timeseriesWithPadding struct {
|
||||
tss []*timeseries
|
||||
|
||||
// The padding prevents false sharing on widespread platforms with
|
||||
// 128 mod (cache line size) = 0 .
|
||||
_ [128 - unsafe.Sizeof([]*timeseries{})%128]byte
|
||||
// The padding prevents false sharing
|
||||
_ [atomicutil.CacheLineSize - unsafe.Sizeof([]*timeseries{})%atomicutil.CacheLineSize]byte
|
||||
}
|
||||
|
||||
type timeseriesByWorkerID struct {
|
||||
|
||||
@@ -529,7 +529,7 @@ type rollupFuncArg struct {
|
||||
timestamps []int64
|
||||
|
||||
// Real value preceding values.
|
||||
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
|
||||
// Is populated if preceding value is within the rc.LookbackDelta.
|
||||
realPrevValue float64
|
||||
|
||||
// Real value which goes after values.
|
||||
@@ -776,13 +776,18 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
|
||||
rfa.realPrevValue = nan
|
||||
if i > 0 {
|
||||
prevValue, prevTimestamp := values[i-1], timestamps[i-1]
|
||||
// set realPrevValue if rc.LookbackDelta == 0
|
||||
// or if distance between datapoint in prev interval and beginning of this interval
|
||||
// set realPrevValue if rc.LookbackDelta == 0 or
|
||||
// if distance between datapoint in prev interval and first datapoint in this interval
|
||||
// doesn't exceed LookbackDelta.
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1381
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/894
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8045
|
||||
if rc.LookbackDelta == 0 || (tStart-prevTimestamp) < rc.LookbackDelta {
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8935
|
||||
currTimestamp := tStart
|
||||
if len(rfa.timestamps) > 0 {
|
||||
currTimestamp = rfa.timestamps[0]
|
||||
}
|
||||
if rc.LookbackDelta == 0 || (currTimestamp-prevTimestamp) < rc.LookbackDelta {
|
||||
rfa.realPrevValue = prevValue
|
||||
}
|
||||
}
|
||||
@@ -1826,14 +1831,18 @@ func rollupIncreasePure(rfa *rollupFuncArg) float64 {
|
||||
// There is no need in handling NaNs here, since they must be cleaned up
|
||||
// before calling rollup funcs.
|
||||
values := rfa.values
|
||||
// restore to the real value because of potential staleness reset
|
||||
prevValue := rfa.realPrevValue
|
||||
prevValue := rfa.prevValue
|
||||
if math.IsNaN(prevValue) {
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
}
|
||||
// Assume the counter starts from 0.
|
||||
prevValue = 0
|
||||
if !math.IsNaN(rfa.realPrevValue) {
|
||||
// Assume that the value didn't change during the current gap
|
||||
// if realPrevValue exists.
|
||||
prevValue = rfa.realPrevValue
|
||||
}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
// Assume the counter didn't change since prevValue.
|
||||
|
||||
@@ -1719,6 +1719,33 @@ func TestRollupDeltaWithStaleness(t *testing.T) {
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
t.Run("issue-8935", func(t *testing.T) {
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8935
|
||||
// below dataset has a gap that exceeds LookbackDelta.
|
||||
// The step is picked in a way that on [60e3-90e3] window
|
||||
// the prevValue will be NaN, but 60e3-55e3 still matches
|
||||
// timestamp=10e3 and stores its value as realPrevValue.
|
||||
// This results into delta=1-50=-49 increase result.
|
||||
// The fix makes it to deduct LookbackDelta not from window start
|
||||
// but from first captured data point in the window, so it becomes 70e3-55e3=15e3.
|
||||
// And realPrevValue becomes NaN due to staleness detection.
|
||||
timestamps = []int64{0, 10000, 70000, 80000}
|
||||
values = []float64{50, 50, 1, 1}
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 90e3,
|
||||
Step: 30e3,
|
||||
LookbackDelta: 55e3,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, _ := rc.Do(nil, values, timestamps)
|
||||
valuesExpected := []float64{0, 0, 0, 1}
|
||||
timestampsExpected := []int64{0, 30e3, 60e3, 90e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRollupIncreasePureWithStaleness(t *testing.T) {
|
||||
|
||||
@@ -66,8 +66,8 @@ or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/victo
|
||||
The list of MetricsQL features on top of PromQL:
|
||||
|
||||
* Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#selecting-graphite-metrics).
|
||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#graphite-api-usage) for details.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
||||
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
||||
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
||||
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
app/vmselect/vmui/assets/index-C85_NB5q.css
Normal file
1
app/vmselect/vmui/assets/index-C85_NB5q.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
209
app/vmselect/vmui/assets/index-xmjGcv4-.js
Normal file
209
app/vmselect/vmui/assets/index-xmjGcv4-.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
67
app/vmselect/vmui/assets/vendor-D8IJGiEn.js
Normal file
67
app/vmselect/vmui/assets/vendor-D8IJGiEn.js
Normal file
File diff suppressed because one or more lines are too long
@@ -36,10 +36,10 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-C_-w5pCZ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-C-vZmbyg.js">
|
||||
<script type="module" crossorigin src="./assets/index-xmjGcv4-.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Brup_hCI.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -23,6 +24,7 @@ export default [...compat.extends(
|
||||
plugins: {
|
||||
react,
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
"unused-imports": unusedImports,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
@@ -59,7 +61,7 @@ export default [...compat.extends(
|
||||
allowTernary: true
|
||||
}],
|
||||
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||
"@typescript-eslint/no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "none",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
@@ -67,6 +69,8 @@ export default [...compat.extends(
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}],
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
|
||||
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
||||
|
||||
@@ -85,6 +89,7 @@ export default [...compat.extends(
|
||||
quotes: ["error", "double"],
|
||||
semi: ["error", "always"],
|
||||
"react/prop-types": 0,
|
||||
"react/react-in-jsx-scope": "off",
|
||||
|
||||
},
|
||||
}];
|
||||
|
||||
57
app/vmui/packages/vmui/package-lock.json
generated
57
app/vmui/packages/vmui/package-lock.json
generated
@@ -23,9 +23,9 @@
|
||||
"preact": "^10.26.5",
|
||||
"qs": "^6.14.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"uplot": "^1.6.32",
|
||||
"vite": "^6.2.6",
|
||||
"vite": "^6.2.7",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,6 +42,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"globals": "^16.0.0",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"jsdom": "^26.1.0",
|
||||
@@ -2115,12 +2116,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
@@ -4199,6 +4194,22 @@
|
||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unused-imports": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
|
||||
"integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
|
||||
"eslint": "^9.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
||||
@@ -6476,15 +6487,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
|
||||
"integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
|
||||
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -6500,12 +6509,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz",
|
||||
"integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz",
|
||||
"integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.5.0"
|
||||
"react-router": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -8012,12 +8021,6 @@
|
||||
"devOptional": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -8204,9 +8207,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
|
||||
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
|
||||
"version": "6.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.7.tgz",
|
||||
"integrity": "sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -20,14 +20,15 @@
|
||||
"preact": "^10.26.5",
|
||||
"qs": "^6.14.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"uplot": "^1.6.32",
|
||||
"vite": "^6.2.6",
|
||||
"vite": "^6.2.7",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "npm run copy-metricsql-docs",
|
||||
"start": "vite",
|
||||
"start:playground": "cross-env PLAYGROUND=METRICS npm run start",
|
||||
"start:logs": "vite --mode victorialogs",
|
||||
"start:logs:playground": "cross-env PLAYGROUND=LOGS npm run start:logs",
|
||||
"start:anomaly": "vite --mode vmanomaly",
|
||||
@@ -66,6 +67,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"globals": "^16.0.0",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"jsdom": "^26.1.0",
|
||||
|
||||
@@ -66,8 +66,8 @@ or at your own [VictoriaMetrics instance](https://docs.victoriametrics.com/victo
|
||||
The list of MetricsQL features on top of PromQL:
|
||||
|
||||
* Graphite-compatible filters can be passed via `{__graphite__="foo.*.bar"}` syntax.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#selecting-graphite-metrics).
|
||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite#graphite-api-usage) for details.
|
||||
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
|
||||
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
|
||||
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
|
||||
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
|
||||
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
|
||||
|
||||
@@ -69,7 +69,7 @@ const LogsQueryEditorAutocomplete: FC<QueryEditorAutocompleteProps> = ({
|
||||
if (insertType === ContextType.FilterName) {
|
||||
modifiedInsert += ":";
|
||||
} else if (contextType === ContextType.FilterValue) {
|
||||
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `"${modifiedInsert}"`;
|
||||
const insertWithQuotes = value.startsWith("_stream:") ? modifiedInsert : `${JSON.stringify(modifiedInsert)}`;
|
||||
modifiedInsert = `${contextData?.filterName || ""}:${insertWithQuotes}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { hasSortPipe } from "./sort";
|
||||
|
||||
describe("hasSortPipe()", () => {
|
||||
/** Queries that MUST be recognised as containing a sort/order pipe. */
|
||||
const positive: string[] = [
|
||||
// ───── basic usage ─────
|
||||
"sort by (_time)",
|
||||
"| sort by (_time)",
|
||||
"|sort(_time) desc",
|
||||
"| order by (foo desc)",
|
||||
"_time:5m | sort by (_stream, _time)",
|
||||
|
||||
// ───── documented options ─────
|
||||
"_time:1h | sort by (request_duration desc) limit 10",
|
||||
"_time:1h | sort by (request_duration desc) partition by (host) limit 3",
|
||||
"_time:5m | sort by (_time) rank as position",
|
||||
|
||||
// ───── whitespace / tabs ─────
|
||||
"|\t sort\tby (host)",
|
||||
|
||||
// ───── no space after the pipe ─────
|
||||
"foo|sort by (_time)",
|
||||
];
|
||||
|
||||
/** Queries that MUST **not** be recognised (false positives). */
|
||||
const negative: string[] = [
|
||||
"", // empty
|
||||
"error | sample 100", // no sort
|
||||
"|sorted(field)", // 'sorted' ≠ 'sort'
|
||||
"|sorter(field)", // 'sorter' ≠ 'sort'
|
||||
"my_sort(field)", // function name
|
||||
"| sorta by (field)", // 'sorta'
|
||||
"foo | orderliness by (bar)", // 'orderliness' ≠ 'order'
|
||||
];
|
||||
|
||||
it.each(positive)("detects pipe in ➜ %s", query => {
|
||||
expect(hasSortPipe(query)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(negative)("does NOT detect pipe in ➜ %s", query => {
|
||||
expect(hasSortPipe(query)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
const hasSortPipeRe = /(?:^|\|)\s*(?:sort|order)\b/i;
|
||||
|
||||
export function hasSortPipe(query: string): boolean {
|
||||
return hasSortPipeRe.test(query);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import { FC, useMemo, useState } from "preact/compat";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { RestartIcon, SettingsIcon } from "../../Main/Icons";
|
||||
import Button from "../../Main/Button/Button";
|
||||
@@ -62,7 +62,8 @@ const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
|
||||
].some(Boolean);
|
||||
|
||||
const logsKeys = useMemo(() => {
|
||||
return Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
|
||||
const uniqueKeys = new Set(logs.map(l => Object.keys(l)).flat());
|
||||
return Array.from(uniqueKeys).sort((a, b) => a.localeCompare(b));
|
||||
}, [logs]);
|
||||
|
||||
const {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useMemo, useRef } from "preact/compat";
|
||||
import { FC, useMemo, useRef } from "preact/compat";
|
||||
import { ArrowDropDownIcon } from "../../Icons";
|
||||
import useBoolean from "../../../../hooks/useBoolean";
|
||||
import Popper from "../../Popper/Popper";
|
||||
@@ -10,11 +10,12 @@ interface SelectLimitProps {
|
||||
limit: number | string;
|
||||
allowUnlimited?: boolean;
|
||||
onChange: (val: number) => void;
|
||||
onOpenSelect?: () => void;
|
||||
}
|
||||
|
||||
const defaultLimits = [10, 25, 50, 100, 250, 500, 1000];
|
||||
|
||||
const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange }) => {
|
||||
const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange, onOpenSelect }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -28,6 +29,11 @@ const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange })
|
||||
setFalse: handleClose,
|
||||
} = useBoolean(false);
|
||||
|
||||
const handleClickSelect = () => {
|
||||
toggleOpenList();
|
||||
if(!openList) onOpenSelect?.();
|
||||
};
|
||||
|
||||
const handleChangeLimit = (n: number) => () => {
|
||||
onChange(n);
|
||||
handleClose();
|
||||
@@ -37,7 +43,7 @@ const SelectLimit: FC<SelectLimitProps> = ({ limit, allowUnlimited, onChange })
|
||||
<>
|
||||
<div
|
||||
className="vm-select-limits-button"
|
||||
onClick={toggleOpenList}
|
||||
onClick={handleClickSelect}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect, useRef, useMemo } from "preact/compat";
|
||||
import { FC, useEffect, useRef, useMemo } from "preact/compat";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { SearchIcon, SettingsIcon } from "../../Main/Icons";
|
||||
import "./style.scss";
|
||||
@@ -49,8 +49,8 @@ const TableSettings: FC<TableSettingsProps> = ({
|
||||
|
||||
const filteredColumns = useMemo(() => {
|
||||
const allColumns = customColumns.concat(columns);
|
||||
if (!searchColumn) return allColumns;
|
||||
return allColumns.filter(col => col.includes(searchColumn));
|
||||
const result = searchColumn ? allColumns.filter(col => col.includes(searchColumn)) : allColumns;
|
||||
return result.sort((a, b) => a.localeCompare(b));
|
||||
}, [columns, customColumns, searchColumn]);
|
||||
|
||||
const isAllChecked = useMemo(() => {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
&_sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: 40px auto 1fr;
|
||||
box-shadow: $color-background-body 0 1px 1px 0px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import { useCallback } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import DownloadButton from "../../../components/DownloadButton/DownloadButton";
|
||||
@@ -7,10 +7,11 @@ import { downloadCSV, downloadJSON } from "../../../utils/file";
|
||||
import { Logs } from "../../../api/types";
|
||||
|
||||
interface DownloadLogsButtonProps {
|
||||
logs: Logs[];
|
||||
/** Callback to get logs to download */
|
||||
getLogs: () => Logs[];
|
||||
}
|
||||
|
||||
const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
|
||||
const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ getLogs }) => {
|
||||
const { fileExtensions, getDownloaderByExtension } = useMemo(() => {
|
||||
const downloadFileOptions: {
|
||||
extension: string;
|
||||
@@ -39,12 +40,13 @@ const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const logs = getLogs();
|
||||
const downloader = getDownloaderByExtension(fileExtension);
|
||||
if (downloader){
|
||||
const timestamp = dayjs().utc().format(DATE_FILENAME_FORMAT);
|
||||
downloader(logs, `vmui_logs_${timestamp}.${fileExtension}`);
|
||||
}
|
||||
}, [logs]);
|
||||
}, [getLogs]);
|
||||
|
||||
return <DownloadButton
|
||||
title={"Download logs"}
|
||||
@@ -53,4 +55,4 @@ const DownloadLogsButton: FC<DownloadLogsButtonProps> = ({ logs }) => {
|
||||
/>;
|
||||
};
|
||||
|
||||
export default DownloadLogsButton;
|
||||
export default DownloadLogsButton;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||
import { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||
import ExploreLogsBody from "./ExploreLogsBody/ExploreLogsBody";
|
||||
import useStateSearchParams from "../../hooks/useStateSearchParams";
|
||||
import useSearchParamsFromObject from "../../hooks/useSearchParamsFromObject";
|
||||
@@ -18,6 +18,7 @@ import { useSearchParams } from "react-router-dom";
|
||||
import { useQueryDispatch, useQueryState } from "../../state/query/QueryStateContext";
|
||||
import { getUpdatedHistory } from "../../components/QueryHistory/utils";
|
||||
import { useDebounceCallback } from "../../hooks/useDebounceCallback";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
|
||||
const storageLimit = Number(getFromStorage("LOGS_LIMIT"));
|
||||
const defaultLimit = isNaN(storageLimit) ? LOGS_ENTRIES_LIMIT : storageLimit;
|
||||
@@ -30,6 +31,7 @@ const ExploreLogs: FC = () => {
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
const [searchParams] = useSearchParams();
|
||||
const hideChart = useMemo(() => searchParams.get("hide_chart"), [searchParams]);
|
||||
const prevHideChart = usePrevious(hideChart);
|
||||
|
||||
const [limit, setLimit] = useStateSearchParams(defaultLimit, "limit");
|
||||
const [query, setQuery] = useStateSearchParams("*", "query");
|
||||
@@ -118,11 +120,10 @@ const ExploreLogs: FC = () => {
|
||||
}, [query, isUpdatingQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideChart) debouncedFetchLogs(period, true);
|
||||
return () => {
|
||||
debouncedFetchLogs.cancel?.();
|
||||
};
|
||||
}, [hideChart, period]);
|
||||
if (!hideChart && prevHideChart) {
|
||||
fetchLogHits(period);
|
||||
}
|
||||
}, [hideChart, prevHideChart, period]);
|
||||
|
||||
return (
|
||||
<div className="vm-explore-logs">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC, useState, useMemo, useRef } from "preact/compat";
|
||||
import { CodeIcon, ListIcon, TableIcon } from "../../../components/Main/Icons";
|
||||
import { FC, useRef } from "preact/compat";
|
||||
import { CodeIcon, ListIcon, TableIcon, PlayIcon } from "../../../components/Main/Icons";
|
||||
import Tabs from "../../../components/Main/Tabs/Tabs";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
@@ -7,18 +7,11 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import { Logs } from "../../../api/types";
|
||||
import useStateSearchParams from "../../../hooks/useStateSearchParams";
|
||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
||||
import TableSettings from "../../../components/Table/TableSettings/TableSettings";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import TableLogs from "./TableLogs";
|
||||
import GroupLogs from "../GroupLogs/GroupLogs";
|
||||
import JsonView from "../../../components/Views/JsonView/JsonView";
|
||||
import LineLoader from "../../../components/Main/LineLoader/LineLoader";
|
||||
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
|
||||
import DownloadLogsButton from "../DownloadLogsButton/DownloadLogsButton";
|
||||
|
||||
const MemoizedTableLogs = React.memo(TableLogs);
|
||||
const MemoizedGroupLogs = React.memo(GroupLogs);
|
||||
const MemoizedJsonView = React.memo(JsonView);
|
||||
import GroupView from "./views/GroupView/GroupView";
|
||||
import TableView from "./views/TableView/TableView";
|
||||
import JsonView from "./views/JsonView/JsonView";
|
||||
import LiveTailingView from "./views/LiveTailingView/LiveTailingView";
|
||||
|
||||
export interface ExploreLogBodyProps {
|
||||
data: Logs[];
|
||||
@@ -29,44 +22,28 @@ enum DisplayType {
|
||||
group = "group",
|
||||
table = "table",
|
||||
json = "json",
|
||||
liveTailing = "liveTailing",
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ label: "Group", value: DisplayType.group, icon: <ListIcon/> },
|
||||
{ label: "Table", value: DisplayType.table, icon: <TableIcon/> },
|
||||
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/> },
|
||||
{ label: "Group", value: DisplayType.group, icon: <ListIcon/>, Component: GroupView },
|
||||
{ label: "Table", value: DisplayType.table, icon: <TableIcon/>, Component: TableView },
|
||||
{ label: "JSON", value: DisplayType.json, icon: <CodeIcon/>, Component: JsonView },
|
||||
{ label: "Live", value: DisplayType.liveTailing, icon: <PlayIcon/>, Component: LiveTailingView },
|
||||
];
|
||||
|
||||
const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
const groupSettingsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [activeTab, setActiveTab] = useStateSearchParams(DisplayType.group, "view");
|
||||
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
|
||||
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(1000, "rows_per_page");
|
||||
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
if (!data?.length || activeTab !== DisplayType.table) return [];
|
||||
const keys = new Set<string>();
|
||||
for (const item of data) {
|
||||
for (const key in item) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return Array.from(keys);
|
||||
}, [data, activeTab]);
|
||||
const settingsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleChangeTab = (view: string) => {
|
||||
setActiveTab(view as DisplayType);
|
||||
setSearchParamsFromKeys({ view });
|
||||
};
|
||||
|
||||
const handleSetRowsPerPage = (limit: number) => {
|
||||
setRowsPerPage(limit);
|
||||
setSearchParamsFromKeys({ rows_per_page: limit });
|
||||
};
|
||||
const ActiveTabComponent = tabs.find(tab => tab.value === activeTab)?.Component;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -95,39 +72,16 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||
items={tabs}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
<div className="vm-explore-logs-body-header__log-info">
|
||||
Total logs returned: <b>{data.length}</b>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === DisplayType.table && (
|
||||
<div className="vm-explore-logs-body-header__settings">
|
||||
<SelectLimit
|
||||
limit={rowsPerPage}
|
||||
onChange={handleSetRowsPerPage}
|
||||
/>
|
||||
<div className="vm-explore-logs-body-header__table-settings">
|
||||
{data.length > 0 && <DownloadLogsButton logs={data} />}
|
||||
<TableSettings
|
||||
columns={columns}
|
||||
selectedColumns={displayColumns}
|
||||
onChangeColumns={setDisplayColumns}
|
||||
tableCompact={tableCompact}
|
||||
toggleTableCompact={toggleTableCompact}
|
||||
/>
|
||||
{activeTab !== DisplayType.liveTailing && (
|
||||
<div className="vm-explore-logs-body-header__log-info">
|
||||
Total logs returned: <b>{data.length}</b>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === DisplayType.group && (
|
||||
<>
|
||||
<div
|
||||
className="vm-explore-logs-body-header__settings"
|
||||
ref={groupSettingsRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activeTab === DisplayType.json && data.length > 0 && (
|
||||
<DownloadLogsButton logs={data} />
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="vm-explore-logs-body-header__settings"
|
||||
ref={settingsRef}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -136,29 +90,12 @@ const ExploreLogsBody: FC<ExploreLogBodyProps> = ({ data, isLoading }) => {
|
||||
"vm-explore-logs-body__table_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{!data.length && <div className="vm-explore-logs-body__empty">No logs found</div>}
|
||||
{!!data.length && (
|
||||
<>
|
||||
{activeTab === DisplayType.table && (
|
||||
<MemoizedTableLogs
|
||||
logs={data}
|
||||
displayColumns={displayColumns}
|
||||
tableCompact={tableCompact}
|
||||
columns={columns}
|
||||
rowsPerPage={Number(rowsPerPage)}
|
||||
/>
|
||||
)}
|
||||
{activeTab === DisplayType.group && (
|
||||
<MemoizedGroupLogs
|
||||
logs={data}
|
||||
settingsRef={groupSettingsRef}
|
||||
/>
|
||||
)}
|
||||
{activeTab === DisplayType.json && (
|
||||
<MemoizedJsonView data={data}/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{ActiveTabComponent &&
|
||||
<ActiveTabComponent
|
||||
data={data}
|
||||
settingsRef={settingsRef}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
position: relative;
|
||||
|
||||
&-header {
|
||||
background-color: $color-background-block;
|
||||
z-index: 1;
|
||||
margin: -$padding-medium 0-$padding-medium 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@media (max-width:1000px) {
|
||||
top: 51px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
margin: -$padding-global 0-$padding-global 0;
|
||||
@@ -19,11 +27,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__table-settings {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&__log-info {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Logs } from "../../../api/types";
|
||||
|
||||
export interface ViewProps {
|
||||
data: Logs[];
|
||||
settingsRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import GroupLogs from "../../../GroupLogs/GroupLogs";
|
||||
import { ViewProps } from "../../types";
|
||||
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
|
||||
|
||||
const MemoizedGroupLogs = React.memo(GroupLogs);
|
||||
|
||||
const GroupView: FC<ViewProps> = ({ data, settingsRef }) => {
|
||||
if (!data.length) return <EmptyLogs />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MemoizedGroupLogs
|
||||
logs={data}
|
||||
settingsRef={settingsRef}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupView;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
|
||||
import { createPortal } from "preact/compat";
|
||||
import JsonViewComponent from "../../../../../components/Views/JsonView/JsonView";
|
||||
import { ViewProps } from "../../types";
|
||||
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const MemoizedJsonView = React.memo(JsonViewComponent);
|
||||
|
||||
const JsonView: FC<ViewProps> = ({ data, settingsRef }) => {
|
||||
const getLogs = useCallback(() => data, [data]);
|
||||
|
||||
const renderSettings = () => {
|
||||
if (!settingsRef.current) return null;
|
||||
|
||||
return createPortal(
|
||||
data.length > 0 && <DownloadLogsButton getLogs={getLogs} />,
|
||||
settingsRef.current
|
||||
);
|
||||
};
|
||||
|
||||
if (!data.length) return <EmptyLogs />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderSettings()}
|
||||
<MemoizedJsonView data={data} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonView;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { FC, RefObject, useCallback, useRef } from "preact/compat";
|
||||
import { createPortal } from "preact/compat";
|
||||
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
|
||||
import Button from "../../../../../components/Main/Button/Button";
|
||||
import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
|
||||
import { DeleteIcon, PauseIcon, PlayCircleOutlineIcon, SettingsIcon } from "../../../../../components/Main/Icons";
|
||||
import Tooltip from "../../../../../components/Main/Tooltip/Tooltip";
|
||||
import Modal from "../../../../../components/Main/Modal/Modal";
|
||||
import Switch from "../../../../../components/Main/Switch/Switch";
|
||||
import useBoolean from "../../../../../hooks/useBoolean";
|
||||
import { Logs } from "../../../../../api/types";
|
||||
|
||||
interface LiveTailingSettingsProps {
|
||||
settingsRef: RefObject<HTMLDivElement>;
|
||||
rowsPerPage: number;
|
||||
handleSetRowsPerPage: (limit: number) => void;
|
||||
logs: Logs[];
|
||||
isPaused: boolean;
|
||||
handleResumeLiveTailing: () => void;
|
||||
pauseLiveTailing: () => void;
|
||||
clearLogs: () => void;
|
||||
isCompactTailingNumber: boolean;
|
||||
handleSetCompactTailing: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const LiveTailingSettings: FC<LiveTailingSettingsProps> = ({
|
||||
settingsRef,
|
||||
rowsPerPage,
|
||||
handleSetRowsPerPage,
|
||||
logs,
|
||||
isPaused,
|
||||
handleResumeLiveTailing,
|
||||
pauseLiveTailing,
|
||||
clearLogs,
|
||||
isCompactTailingNumber,
|
||||
handleSetCompactTailing
|
||||
}) => {
|
||||
const settingButtonRef = useRef<HTMLDivElement>(null);
|
||||
const { value: isSettingsOpen, setFalse: closeSettings, setTrue: openSettings } = useBoolean(false);
|
||||
|
||||
const getLogs = useCallback(() => logs.map(({ _log_id, ...log }) => log), [logs]);
|
||||
|
||||
if (!settingsRef.current) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="vm-live-tailing-view__settings">
|
||||
<SelectLimit
|
||||
limit={rowsPerPage}
|
||||
onChange={handleSetRowsPerPage}
|
||||
onOpenSelect={pauseLiveTailing}
|
||||
/>
|
||||
<div className="vm-live-tailing-view__settings-buttons">
|
||||
{logs.length > 0 && <DownloadLogsButton getLogs={getLogs}/>}
|
||||
{isPaused ? (
|
||||
<Tooltip
|
||||
title={"Resume live tailing"}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
onClick={handleResumeLiveTailing}
|
||||
startIcon={<PlayCircleOutlineIcon/>}
|
||||
ariaLabel={"Resume live tailing"}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={"Pause live tailing"}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
onClick={pauseLiveTailing}
|
||||
startIcon={<PauseIcon/>}
|
||||
ariaLabel={"Pause live tailing"}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
title={"Clear logs"}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="secondary"
|
||||
onClick={clearLogs}
|
||||
startIcon={<DeleteIcon/>}
|
||||
ariaLabel={"Clear logs"}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Settings"}
|
||||
>
|
||||
<Button
|
||||
ref={settingButtonRef}
|
||||
variant="text"
|
||||
color="secondary"
|
||||
onClick={openSettings}
|
||||
startIcon={<SettingsIcon/>}
|
||||
ariaLabel={"Settings"}
|
||||
/>
|
||||
</Tooltip>
|
||||
{isSettingsOpen && <Modal
|
||||
onClose={closeSettings}
|
||||
title={"Live tailing settings"}
|
||||
>
|
||||
<div className="vm-live-tailing-view__settings-modal">
|
||||
<div className={"vm-live-tailing-view__settings-modal-item"}>
|
||||
<Switch
|
||||
label={"Expandable Properties View"}
|
||||
value={isCompactTailingNumber}
|
||||
onChange={handleSetCompactTailing}
|
||||
/>
|
||||
<span className="vm-group-logs-configurator-item__info">
|
||||
Switches log display to expandable properties view with additional visualization settings. Please note: when processing large volumes of data, it may increase system response time.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>}
|
||||
</div>
|
||||
</div>,
|
||||
settingsRef.current
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveTailingSettings;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import { ViewProps } from "../../types";
|
||||
import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
|
||||
import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
|
||||
import "./style.scss";
|
||||
import { useLiveTailingLogs } from "./useLiveTailingLogs";
|
||||
import { LOGS_DISPLAY_FIELDS, LOGS_URL_PARAMS } from "../../../../../constants/logs";
|
||||
import { useMemo } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import throttle from "lodash/throttle";
|
||||
import GroupLogsItem from "../../../GroupLogs/GroupLogsItem";
|
||||
import LiveTailingSettings from "./LiveTailingSettings";
|
||||
import Alert from "../../../../../components/Main/Alert/Alert";
|
||||
import { isDecreasing } from "../../../../../utils/array";
|
||||
|
||||
const SCROLL_THRESHOLD = 100;
|
||||
const scrollToBottom = () => window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior: "instant"
|
||||
});
|
||||
const throttledScrollToBottom = throttle(scrollToBottom, 200);
|
||||
|
||||
const LiveTailingView: FC<ViewProps> = ({ settingsRef }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const [searchParams] = useSearchParams();
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
|
||||
const [query, _setQuery] = useStateSearchParams("*", "query");
|
||||
const [isCompactTailingStr] = useStateSearchParams(0, "compact_tailing");
|
||||
const isCompactTailingNumber = Boolean(Number(isCompactTailingStr));
|
||||
const {
|
||||
logs,
|
||||
isPaused,
|
||||
error,
|
||||
startLiveTailing,
|
||||
stopLiveTailing,
|
||||
pauseLiveTailing,
|
||||
resumeLiveTailing,
|
||||
clearLogs,
|
||||
isLimitedLogsPerUpdate
|
||||
} = useLiveTailingLogs(query, rowsPerPage);
|
||||
|
||||
const displayFieldsString = searchParams.get(LOGS_URL_PARAMS.DISPLAY_FIELDS) || LOGS_DISPLAY_FIELDS;
|
||||
const displayFields = useMemo(() => displayFieldsString.split(","), [displayFieldsString]);
|
||||
|
||||
const handleResumeLiveTailing = useCallback(() => {
|
||||
throttledScrollToBottom();
|
||||
resumeLiveTailing();
|
||||
}, [resumeLiveTailing]);
|
||||
|
||||
const handleSetRowsPerPage = useCallback((limit: number) => {
|
||||
setSearchParamsFromKeys({ rows_per_page: limit });
|
||||
}, [setRowsPerPage, setSearchParamsFromKeys]);
|
||||
|
||||
const handleSetCompactTailing = useCallback((value: boolean) => {
|
||||
setSearchParamsFromKeys({ compact_tailing: Number(value) });
|
||||
}, [setSearchParamsFromKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
startLiveTailing();
|
||||
return () => stopLiveTailing();
|
||||
}, [startLiveTailing, stopLiveTailing]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
let prevScrollTop: number[] = [];
|
||||
const handleScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
|
||||
const isBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < SCROLL_THRESHOLD;
|
||||
|
||||
setIsAtBottom(isBottom);
|
||||
prevScrollTop.push(scrollTop);
|
||||
prevScrollTop = prevScrollTop.slice(-3);
|
||||
const isMoveToTop = isDecreasing(prevScrollTop);
|
||||
|
||||
if (!isBottom && !isPaused && isMoveToTop) {
|
||||
pauseLiveTailing();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("scroll", handleScroll);
|
||||
return () => document.removeEventListener("scroll", handleScroll);
|
||||
}, [isPaused, pauseLiveTailing, resumeLiveTailing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAtBottom && !isPaused) {
|
||||
throttledScrollToBottom();
|
||||
}
|
||||
}, [logs, isAtBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
handleResumeLiveTailing();
|
||||
}, [rowsPerPage]);
|
||||
|
||||
if (error) {
|
||||
return <div className="vm-live-tailing-view__error">{error}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LiveTailingSettings
|
||||
settingsRef={settingsRef}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleSetRowsPerPage={handleSetRowsPerPage}
|
||||
logs={logs}
|
||||
isPaused={isPaused}
|
||||
handleResumeLiveTailing={handleResumeLiveTailing}
|
||||
pauseLiveTailing={pauseLiveTailing}
|
||||
clearLogs={clearLogs}
|
||||
isCompactTailingNumber={isCompactTailingNumber}
|
||||
handleSetCompactTailing={handleSetCompactTailing}
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="vm-live-tailing-view__container"
|
||||
>
|
||||
{logs.length === 0
|
||||
? (<div className="vm-live-tailing-view__empty">Waiting for logs...</div>)
|
||||
: (<div className="vm-live-tailing-view__logs">
|
||||
{logs.map(({ _log_id, ...log }, idx) =>
|
||||
isCompactTailingNumber
|
||||
? (
|
||||
<GroupLogsItem
|
||||
key={_log_id}
|
||||
log={log}
|
||||
onItemClick={pauseLiveTailing}
|
||||
hideGroupButton={true}
|
||||
displayFields={displayFields}
|
||||
/>
|
||||
) : (
|
||||
<pre
|
||||
key={idx}
|
||||
className="vm-live-tailing-view__log-row"
|
||||
>
|
||||
{JSON.stringify(log)}
|
||||
</pre>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isLimitedLogsPerUpdate && (<Alert variant="warning">Too many logs per second detected. Large volumes of log data are difficult to process and may impact performance. We recommend adding filters to your query for better analysis and system performance.</Alert>)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveTailingView;
|
||||
@@ -0,0 +1,76 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-live-tailing-view {
|
||||
&__settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__settings-modal {
|
||||
max-width: 500px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
&__settings-modal-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $padding-small;
|
||||
}
|
||||
|
||||
&__settings-modal-item-info {
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
&__settings-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
min-height: 200px;
|
||||
font-family: $font-family-monospace;
|
||||
padding-bottom: $padding-medium;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
&__error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
&__logs {
|
||||
.vm-group-logs-row {
|
||||
animation: highlight-fade 1s ease-out forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&__log-row {
|
||||
margin-top: $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlight-fade {
|
||||
0% {
|
||||
background-color: $color-tropical-blue;
|
||||
}
|
||||
100% {
|
||||
background-color: $color-background-block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { act, renderHook } from "@testing-library/preact";
|
||||
import { useLiveTailingLogs } from "./useLiveTailingLogs";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("../../../../../state/common/StateContext", () => ({
|
||||
useAppState: () => ({ serverUrl: "http://localhost:8080" }),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../../hooks/useTenant", () => ({
|
||||
useTenant: () => ({}),
|
||||
}));
|
||||
|
||||
// Mock dependencies
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const createMockStreamResponse = (logs: string[], sendCount: number = 1) => ({
|
||||
ok: true,
|
||||
body: new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < sendCount; i++) {
|
||||
logs.forEach((log) => {
|
||||
controller.enqueue(new TextEncoder().encode(log + "\n"));
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
text: async () => logs.join("\n"),
|
||||
});
|
||||
|
||||
describe("useLiveTailingLogs", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should start live tailing and process logs", async () => {
|
||||
const query = "*";
|
||||
const limit = 10;
|
||||
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
|
||||
|
||||
mockFetch.mockResolvedValue(createMockStreamResponse(["{\"logs\":\"test log\"}"]));
|
||||
|
||||
await act(async () => {
|
||||
const started = await result.current.startLiveTailing();
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/select/logsql/tail",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
query: query.trim(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should pause and resume live tailing", () => {
|
||||
const query = "*";
|
||||
const limit = 10;
|
||||
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
|
||||
|
||||
act(() => {
|
||||
result.current.pauseLiveTailing();
|
||||
});
|
||||
|
||||
expect(result.current.isPaused).toBe(true);
|
||||
|
||||
act(() => {
|
||||
result.current.resumeLiveTailing();
|
||||
});
|
||||
|
||||
expect(result.current.isPaused).toBe(false);
|
||||
});
|
||||
|
||||
it("should stop live tailing", async () => {
|
||||
const query = "*";
|
||||
const limit = 10;
|
||||
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
|
||||
|
||||
act(() => {
|
||||
result.current.stopLiveTailing();
|
||||
});
|
||||
|
||||
expect(result.current.logs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should clear logs", () => {
|
||||
const query = "*";
|
||||
const limit = 10;
|
||||
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
|
||||
|
||||
act(() => {
|
||||
result.current.clearLogs();
|
||||
});
|
||||
|
||||
expect(result.current.logs).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle errors during live tailing", async () => {
|
||||
const query = "*";
|
||||
const limit = 10;
|
||||
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
|
||||
|
||||
mockFetch.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
await act(async () => {
|
||||
const started = await result.current.startLiveTailing();
|
||||
expect(started).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBe("Error: Network error");
|
||||
expect(result.current.logs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should process high load of logs incoming at 100k logs per second", async () => {
|
||||
const query = "*";
|
||||
const limit = 1000;
|
||||
const logCount = 10000; // High log rate
|
||||
const logs = Array.from({ length: logCount }, (_, i) => `{"log": "log message ${i}"}`);
|
||||
const { result } = renderHook(() => useLiveTailingLogs(query, limit));
|
||||
|
||||
mockFetch.mockResolvedValue(createMockStreamResponse(logs, 7));
|
||||
|
||||
await act(async () => {
|
||||
const started = await result.current.startLiveTailing();
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
// Wait for logs to process
|
||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||
|
||||
// Verify logs are limited and processed correctly
|
||||
expect(result.current.logs.length).toBeLessThanOrEqual(limit);
|
||||
// After setting flag isLimitedLogsPerUpdate when more than 200 logs received 5 times in a row,
|
||||
// we take only the last 200 logs, so we get 800 older logs (9200 - 9999) and 200 new logs (9800-9999)
|
||||
expect(result.current.logs[0].log).toStrictEqual("log message 9200");
|
||||
expect(result.current.logs[799].log).toStrictEqual("log message 9999");
|
||||
expect(result.current.isLimitedLogsPerUpdate).toBeTruthy();
|
||||
}, { timeout: 9000 });
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import { ErrorTypes } from "../../../../../types";
|
||||
import { Logs } from "../../../../../api/types";
|
||||
import { useAppState } from "../../../../../state/common/StateContext";
|
||||
import useBoolean from "../../../../../hooks/useBoolean";
|
||||
import { useTenant } from "../../../../../hooks/useTenant";
|
||||
|
||||
/**
|
||||
* Defines the maximum number of consecutive times logs can be fetched above the threshold
|
||||
* before showing a warning notification, and vice versa:
|
||||
* - If logs are fetched above a threshold this many times in a row -> show warning
|
||||
* - If warning is shown, it won't disappear until logs are fetched below a threshold
|
||||
* this many times in a row
|
||||
*
|
||||
* This threshold helps optimize log display performance when dealing with large volumes of logs.
|
||||
* If the threshold is consistently exceeded, users will be prompted to add filters to their query
|
||||
* for better system performance and more focused log analysis.
|
||||
*/
|
||||
const MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND = 5;
|
||||
/**
|
||||
* Defines the log's threshold, after which will be shown a warning notification
|
||||
*/
|
||||
const LOGS_THRESHOLD = 200;
|
||||
const CONNECTION_TIMEOUT_MS = 5000;
|
||||
const PROCESSING_INTERVAL_MS = 1000;
|
||||
|
||||
const createStreamProcessor = (
|
||||
bufferRef: React.MutableRefObject<string>,
|
||||
bufferLinesRef: React.MutableRefObject<string[]>,
|
||||
setError: (error: string) => void,
|
||||
restartTailing: () => Promise<boolean>
|
||||
) => {
|
||||
return async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
|
||||
let lastDataTime = Date.now();
|
||||
|
||||
const connectionCheckInterval = setInterval(() => {
|
||||
const timeSinceLastData = Date.now() - lastDataTime;
|
||||
if (timeSinceLastData > CONNECTION_TIMEOUT_MS) {
|
||||
clearInterval(connectionCheckInterval);
|
||||
restartTailing();
|
||||
return;
|
||||
}
|
||||
}, CONNECTION_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
lastDataTime = Date.now();
|
||||
|
||||
const chunk = new TextDecoder().decode(value);
|
||||
const lines = (bufferRef.current + chunk).split("\n");
|
||||
bufferRef.current = lines.pop() || "";
|
||||
bufferLinesRef.current = [...bufferLinesRef.current, ...lines];
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
console.error("Stream processing error:", e);
|
||||
setError(String(e));
|
||||
}
|
||||
} finally {
|
||||
clearInterval(connectionCheckInterval);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const updateLimitModeTracking = (
|
||||
linesCount: number,
|
||||
attemptsFetchLimitRef: React.MutableRefObject<number>,
|
||||
attemptsFetchLowRef: React.MutableRefObject<number>,
|
||||
isLimitedLogsPerUpdate: boolean,
|
||||
) => {
|
||||
if (linesCount > LOGS_THRESHOLD) {
|
||||
attemptsFetchLimitRef.current++;
|
||||
attemptsFetchLowRef.current = 0;
|
||||
} else {
|
||||
attemptsFetchLowRef.current++;
|
||||
attemptsFetchLimitRef.current = 0;
|
||||
}
|
||||
|
||||
if (attemptsFetchLimitRef.current > MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attemptsFetchLowRef.current > MAX_ATTEMPTS_FETCH_LOGS_PER_SECOND) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isLimitedLogsPerUpdate;
|
||||
};
|
||||
|
||||
const parseLogLines = (lines: string[], counterRef: React.MutableRefObject<bigint>): Logs[] => {
|
||||
return lines
|
||||
.map(line => {
|
||||
try {
|
||||
const parsedLine = line && JSON.parse(line);
|
||||
parsedLine._log_id = counterRef.current++;
|
||||
return parsedLine;
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse "${line}" to JSON\n`, e);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as Logs[];
|
||||
};
|
||||
|
||||
interface ProcessBufferedLogsParams {
|
||||
lines: string[];
|
||||
limit: number;
|
||||
counterRef: React.MutableRefObject<bigint>;
|
||||
attemptsFetchLimitRef: React.MutableRefObject<number>;
|
||||
attemptsFetchLowRef: React.MutableRefObject<number>;
|
||||
setIsLimitedLogsPerUpdate: (isLimited: boolean) => void;
|
||||
setLogs: React.Dispatch<React.SetStateAction<Logs[]>>;
|
||||
bufferLinesRef: React.MutableRefObject<string[]>;
|
||||
isLimitedLogsPerUpdate: boolean;
|
||||
}
|
||||
|
||||
const processBufferedLogs = ({
|
||||
lines,
|
||||
limit,
|
||||
counterRef,
|
||||
attemptsFetchLimitRef,
|
||||
attemptsFetchLowRef,
|
||||
setIsLimitedLogsPerUpdate,
|
||||
setLogs,
|
||||
bufferLinesRef,
|
||||
isLimitedLogsPerUpdate
|
||||
}: ProcessBufferedLogsParams) => {
|
||||
|
||||
const isLimitLogsMode = updateLimitModeTracking(lines.length, attemptsFetchLimitRef, attemptsFetchLowRef, isLimitedLogsPerUpdate);
|
||||
const limitedLines = isLimitLogsMode && lines.length > LOGS_THRESHOLD ? lines.slice(-LOGS_THRESHOLD) : lines;
|
||||
const newLogs = parseLogLines(limitedLines, counterRef);
|
||||
|
||||
setIsLimitedLogsPerUpdate(isLimitLogsMode);
|
||||
setLogs(prevLogs => {
|
||||
const combinedLogs = [...prevLogs, ...newLogs];
|
||||
return combinedLogs.length > limit ? combinedLogs.slice(-limit) : combinedLogs;
|
||||
});
|
||||
bufferLinesRef.current = [];
|
||||
};
|
||||
|
||||
export const useLiveTailingLogs = (query: string, limit: number) => {
|
||||
const { serverUrl } = useAppState();
|
||||
|
||||
const [logs, setLogs] = useState<Logs[]>([]);
|
||||
const { value: isPaused, setTrue: pauseLiveTailing, setFalse: resumeLiveTailing } = useBoolean(false);
|
||||
const tenant = useTenant();
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
const [isLimitedLogsPerUpdate, setIsLimitedLogsPerUpdate] = useState(false);
|
||||
|
||||
const counterRef = useRef<bigint>(0n);
|
||||
const abortControllerRef = useRef(new AbortController());
|
||||
const readerRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const bufferRef = useRef<string>("");
|
||||
const bufferLinesRef = useRef<string[]>([]);
|
||||
const attemptsFetchLimitLogsPerSecondCountRef = useRef<number>(0);
|
||||
const attemptsFetchLowLogsPerSecondCountRef = useRef<number>(0);
|
||||
|
||||
const stopLiveTailing = useCallback(() => {
|
||||
if (readerRef.current) {
|
||||
readerRef.current.cancel();
|
||||
readerRef.current = null;
|
||||
}
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
if (bufferRef.current) {
|
||||
bufferRef.current = "";
|
||||
}
|
||||
abortControllerRef.current.abort();
|
||||
}, []);
|
||||
|
||||
const startLiveTailing = useCallback(async () => {
|
||||
stopLiveTailing();
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
const { signal } = abortControllerRef.current;
|
||||
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}/select/logsql/tail`, {
|
||||
signal,
|
||||
method: "POST",
|
||||
headers: {
|
||||
...tenant,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
query: query.trim(),
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
const text = await response.text();
|
||||
setError(text);
|
||||
setLogs([]);
|
||||
return false;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
readerRef.current = reader;
|
||||
|
||||
const processStream = createStreamProcessor(
|
||||
bufferRef,
|
||||
bufferLinesRef,
|
||||
setError,
|
||||
startLiveTailing
|
||||
);
|
||||
|
||||
processStream(reader);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
setError(String(e));
|
||||
console.error(e);
|
||||
setLogs([]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, [query, stopLiveTailing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaused) {
|
||||
const pauseTimerId = setInterval(() => {
|
||||
if (bufferLinesRef.current.length > limit) {
|
||||
bufferLinesRef.current = bufferLinesRef.current.slice(-limit);
|
||||
}
|
||||
}, PROCESSING_INTERVAL_MS);
|
||||
return () => {
|
||||
clearInterval(pauseTimerId);
|
||||
};
|
||||
}
|
||||
|
||||
const timerId = setInterval(() => {
|
||||
const lines = bufferLinesRef.current;
|
||||
processBufferedLogs({
|
||||
lines,
|
||||
limit,
|
||||
counterRef,
|
||||
attemptsFetchLimitRef: attemptsFetchLimitLogsPerSecondCountRef,
|
||||
attemptsFetchLowRef: attemptsFetchLowLogsPerSecondCountRef,
|
||||
setIsLimitedLogsPerUpdate,
|
||||
isLimitedLogsPerUpdate,
|
||||
setLogs,
|
||||
bufferLinesRef
|
||||
});
|
||||
}, PROCESSING_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(timerId);
|
||||
}, [limit, isPaused, isLimitedLogsPerUpdate]);
|
||||
|
||||
const clearLogs = useCallback(() => {
|
||||
setLogs([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
logs,
|
||||
isPaused,
|
||||
error,
|
||||
startLiveTailing,
|
||||
stopLiveTailing,
|
||||
pauseLiveTailing,
|
||||
resumeLiveTailing,
|
||||
clearLogs,
|
||||
isLimitedLogsPerUpdate
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { FC, useMemo, useState } from "preact/compat";
|
||||
import DownloadLogsButton from "../../../DownloadLogsButton/DownloadLogsButton";
|
||||
import { createPortal } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { ViewProps } from "../../types";
|
||||
import useBoolean from "../../../../../hooks/useBoolean";
|
||||
import useStateSearchParams from "../../../../../hooks/useStateSearchParams";
|
||||
import TableLogs from "../../TableLogs";
|
||||
import SelectLimit from "../../../../../components/Main/Pagination/SelectLimit/SelectLimit";
|
||||
import TableSettings from "../../../../../components/Table/TableSettings/TableSettings";
|
||||
import useSearchParamsFromObject from "../../../../../hooks/useSearchParamsFromObject";
|
||||
import EmptyLogs from "../components/EmptyLogs/EmptyLogs";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const MemoizedTableView = React.memo(TableLogs);
|
||||
|
||||
const TableView: FC<ViewProps> = ({ data, settingsRef }) => {
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
const [displayColumns, setDisplayColumns] = useState<string[]>([]);
|
||||
const [rowsPerPage, setRowsPerPage] = useStateSearchParams(100, "rows_per_page");
|
||||
const { value: tableCompact, toggle: toggleTableCompact } = useBoolean(false);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
for (const item of data) {
|
||||
for (const key in item) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return Array.from(keys).sort((a,b) => a.localeCompare(b));
|
||||
}, [data]);
|
||||
|
||||
const handleSetRowsPerPage = (limit: number) => {
|
||||
setRowsPerPage(limit);
|
||||
setSearchParamsFromKeys({ rows_per_page: limit });
|
||||
};
|
||||
|
||||
const getLogs = useCallback(() => data, [data]);
|
||||
|
||||
const renderSettings = () => {
|
||||
if (!settingsRef.current) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="vm-table-view__settings">
|
||||
<SelectLimit
|
||||
limit={rowsPerPage}
|
||||
onChange={handleSetRowsPerPage}
|
||||
/>
|
||||
<div className="vm-table-view__settings-buttons">
|
||||
{data.length > 0 && <DownloadLogsButton getLogs={getLogs} />}
|
||||
<TableSettings
|
||||
columns={columns}
|
||||
selectedColumns={displayColumns}
|
||||
onChangeColumns={setDisplayColumns}
|
||||
tableCompact={tableCompact}
|
||||
toggleTableCompact={toggleTableCompact}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
settingsRef.current
|
||||
);
|
||||
};
|
||||
|
||||
if (!data.length) return <EmptyLogs />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderSettings()}
|
||||
<MemoizedTableView
|
||||
logs={data}
|
||||
displayColumns={displayColumns}
|
||||
tableCompact={tableCompact}
|
||||
columns={columns}
|
||||
rowsPerPage={Number(rowsPerPage)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableView;
|
||||
@@ -0,0 +1,10 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-table-view {
|
||||
&__settings,
|
||||
&__settings-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
|
||||
const EmptyLogs: FC = () => {
|
||||
return (
|
||||
<div className="vm-explore-logs-body__empty">No logs found</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyLogs;
|
||||
@@ -0,0 +1,12 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-logs-body {
|
||||
&__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
color: $color-text-disabled;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ import Pagination from "../../../components/Main/Pagination/Pagination";
|
||||
import SelectLimit from "../../../components/Main/Pagination/SelectLimit/SelectLimit";
|
||||
import { usePaginateGroups } from "../hooks/usePaginateGroups";
|
||||
import { GroupLogsType } from "../../../types";
|
||||
import { getNanoTimestamp } from "../../../utils/time";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import DownloadLogsButton from "../DownloadLogsButton/DownloadLogsButton";
|
||||
import { hasSortPipe } from "../../../components/Configurators/QueryEditor/LogsQL/utils/sort";
|
||||
|
||||
interface Props {
|
||||
logs: Logs[];
|
||||
@@ -30,6 +30,9 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const query = searchParams.get("query") || "";
|
||||
const queryHasSort = hasSortPipe(query);
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [expandGroups, setExpandGroups] = useState<boolean[]>([]);
|
||||
|
||||
@@ -47,15 +50,10 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||
const streamValue = item.values[0]?.[groupBy] || "";
|
||||
const pairs = getStreamPairs(streamValue);
|
||||
|
||||
// values sorting by time
|
||||
const values = item.values.sort((a, b) => {
|
||||
const aTimestamp = getNanoTimestamp(a._time);
|
||||
const bTimestamp = getNanoTimestamp(b._time);
|
||||
|
||||
if (aTimestamp < bTimestamp) return 1;
|
||||
if (aTimestamp > bTimestamp) return -1;
|
||||
return 0;
|
||||
});
|
||||
// VictoriaLogs sends rows oldest → newest when the query has no `| sort` pipe,
|
||||
// so we reverse the array to put the newest entries first.
|
||||
// If a sort is already specified, keep the original order.
|
||||
const values = queryHasSort ? item.values : item.values.toReversed();
|
||||
|
||||
return {
|
||||
keys: item.keys,
|
||||
@@ -64,8 +62,8 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||
pairs,
|
||||
total: values.length,
|
||||
};
|
||||
}).sort((a, b) => b.values.length - a.values.length); // groups sorting
|
||||
}, [logs, groupBy]);
|
||||
}).sort((a, b) => b.total - a.total); // groups sorting
|
||||
}, [logs, groupBy, queryHasSort]);
|
||||
|
||||
const paginatedGroups = usePaginateGroups(groupData, page, rowsPerPage);
|
||||
|
||||
@@ -96,6 +94,8 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||
window.scrollTo({ top: 0 });
|
||||
};
|
||||
|
||||
const getLogs = useCallback(() => logs, [logs]);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandGroups(new Array(groupData.length).fill(!isMobile));
|
||||
}, [groupData]);
|
||||
@@ -162,7 +162,7 @@ const GroupLogs: FC<Props> = ({ logs, settingsRef }) => {
|
||||
ariaLabel={expandAll ? "Collapse All" : "Expand All"}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DownloadLogsButton logs={logs} />
|
||||
<DownloadLogsButton getLogs={getLogs}/>
|
||||
<GroupLogsConfigurators logs={logs}/>
|
||||
</div>
|
||||
), settingsRef.current)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, memo, useCallback, useEffect, useState } from "preact/compat";
|
||||
import { FC, memo, useCallback, useEffect, useState } from "preact/compat";
|
||||
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
|
||||
import Button from "../../../components/Main/Button/Button";
|
||||
import { CopyIcon, StorageIcon, VisibilityIcon } from "../../../components/Main/Icons";
|
||||
@@ -9,9 +9,10 @@ import { LOGS_GROUP_BY, LOGS_URL_PARAMS } from "../../../constants/logs";
|
||||
interface Props {
|
||||
field: string;
|
||||
value: string;
|
||||
hideGroupButton?: boolean;
|
||||
}
|
||||
|
||||
const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
|
||||
const GroupLogsFieldRow: FC<Props> = ({ field, value, hideGroupButton }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
@@ -75,20 +76,22 @@ const GroupLogsFieldRow: FC<Props> = ({ field, value }) => {
|
||||
size="small"
|
||||
startIcon={isSelectedField ? <VisibilityIcon/> : <VisibilityIcon/>}
|
||||
onClick={handleSelectDisplayField}
|
||||
ariaLabel="copy to clipboard"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={isGroupByField ? "Ungroup this field" : "Group by this field"}>
|
||||
<Button
|
||||
className="vm-group-logs-row-fields-item-controls__button"
|
||||
variant="text"
|
||||
color={isGroupByField ? "secondary" : "gray"}
|
||||
size="small"
|
||||
startIcon={<StorageIcon/>}
|
||||
onClick={handleSelectGroupBy}
|
||||
ariaLabel="copy to clipboard"
|
||||
ariaLabel={isSelectedField ? "Hide this field" : "Show this field instead of the message"}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!hideGroupButton && (
|
||||
<Tooltip title={isGroupByField ? "Ungroup this field" : "Group by this field"}>
|
||||
<Button
|
||||
className="vm-group-logs-row-fields-item-controls__button"
|
||||
variant="text"
|
||||
color={isGroupByField ? "secondary" : "gray"}
|
||||
size="small"
|
||||
startIcon={<StorageIcon/>}
|
||||
onClick={handleSelectGroupBy}
|
||||
ariaLabel={isGroupByField ? "Ungroup this field" : "Group by this field"}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="vm-group-logs-row-fields-item__key">{field}</td>
|
||||
|
||||
@@ -8,9 +8,10 @@ import { getFromStorage } from "../../../utils/storage";
|
||||
|
||||
interface Props {
|
||||
log: Logs;
|
||||
hideGroupButton?: boolean;
|
||||
}
|
||||
|
||||
const GroupLogsFields: FC<Props> = ({ log }) => {
|
||||
const GroupLogsFields: FC<Props> = ({ log, hideGroupButton }) => {
|
||||
const sortedFields = useMemo(() => {
|
||||
return Object.entries(log)
|
||||
.sort(([aKey], [bKey]) => aKey.localeCompare(bKey));
|
||||
@@ -41,6 +42,7 @@ const GroupLogsFields: FC<Props> = ({ log }) => {
|
||||
key={key}
|
||||
field={key}
|
||||
value={value}
|
||||
hideGroupButton={hideGroupButton}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -18,9 +18,11 @@ import GroupLogsFields from "./GroupLogsFields";
|
||||
interface Props {
|
||||
log: Logs;
|
||||
displayFields?: string[];
|
||||
hideGroupButton?: boolean;
|
||||
onItemClick?: (log: Logs) => void;
|
||||
}
|
||||
|
||||
const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
|
||||
const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"], onItemClick, hideGroupButton }) => {
|
||||
const {
|
||||
value: isOpenFields,
|
||||
toggle: toggleOpenFields,
|
||||
@@ -75,6 +77,11 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
toggleOpenFields();
|
||||
onItemClick?.(log);
|
||||
};
|
||||
|
||||
useEventListener("storage", handleUpdateStage);
|
||||
|
||||
return (
|
||||
@@ -84,7 +91,7 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
|
||||
"vm-group-logs-row-content": true,
|
||||
"vm-group-logs-row-content_interactive": !disabledHovers,
|
||||
})}
|
||||
onClick={toggleOpenFields}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{hasFields && (
|
||||
<div
|
||||
@@ -123,7 +130,10 @@ const GroupLogsItem: FC<Props> = ({ log, displayFields = ["_msg"] }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{hasFields && isOpenFields && <GroupLogsFields log={log}/>}
|
||||
{hasFields && isOpenFields && <GroupLogsFields
|
||||
hideGroupButton={hideGroupButton}
|
||||
log={log}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,7 +101,7 @@ const Relabel: FC = () => {
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling"
|
||||
href="https://docs.victoriametrics.com/victoriametrics/relabeling/"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
@@ -154,13 +154,13 @@ const Relabel: FC = () => {
|
||||
<div className="vm-relabeling-steps-item__row">
|
||||
<span>Input Labels:</span>
|
||||
<code>
|
||||
<pre dangerouslySetInnerHTML={{ __html: step.inLabels }}/>
|
||||
<pre dangerouslySetInnerHTML={{ __html: step.errors?.inLabels || step.inLabels }}/>
|
||||
</code>
|
||||
</div>
|
||||
<div className="vm-relabeling-steps-item__row">
|
||||
<span>Output labels:</span>
|
||||
<code>
|
||||
<pre dangerouslySetInnerHTML={{ __html: step.outLabels }}/>
|
||||
<pre dangerouslySetInnerHTML={{ __html: step.errors?.outLabels || step.outLabels }}/>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -138,6 +138,10 @@ export interface RelabelStep {
|
||||
rule: string;
|
||||
inLabels: string;
|
||||
outLabels: string;
|
||||
errors: {
|
||||
inLabels: string;
|
||||
outLabels: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RelabelData {
|
||||
|
||||
36
app/vmui/packages/vmui/src/utils/array.test.ts
Normal file
36
app/vmui/packages/vmui/src/utils/array.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isDecreasing } from "./array";
|
||||
|
||||
describe("isDecreasing", () => {
|
||||
it("should return true for an array with strictly decreasing numbers", () => {
|
||||
expect(isDecreasing([5, 4, 3, 2, 1])).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for an array with increasing numbers", () => {
|
||||
expect(isDecreasing([1, 2, 3, 4, 5])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for an array with equal consecutive numbers", () => {
|
||||
expect(isDecreasing([5, 5, 4, 3, 2])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for an empty array", () => {
|
||||
expect(isDecreasing([])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for an array with a single element", () => {
|
||||
expect(isDecreasing([1])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for an array with both increasing and decreasing numbers", () => {
|
||||
expect(isDecreasing([5, 3, 4, 2, 1])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for an array with negative strictly decreasing numbers", () => {
|
||||
expect(isDecreasing([-1, -2, -3, -4])).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for an array with a mix of positive and negative numbers that do not strictly decrease", () => {
|
||||
expect(isDecreasing([3, 2, -1, -1])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
export const arrayEquals = (a: (string|number)[], b: (string|number)[]) => {
|
||||
export const arrayEquals = (a: (string | number)[], b: (string | number)[]) => {
|
||||
return a.length === b.length && a.every((val, index) => val === b[index]);
|
||||
};
|
||||
|
||||
@@ -17,3 +17,8 @@ export function groupByMultipleKeys<T>(items: T[], keys: (keyof T)[]): { keys: s
|
||||
}));
|
||||
}
|
||||
|
||||
export const isDecreasing = (arr: number[]): boolean => {
|
||||
if (arr.length < 2) return false;
|
||||
|
||||
return arr.every((v, i) => i === 0 || v < arr[i - 1]);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"downlevelIteration": true,
|
||||
"noUnusedLocals": true,
|
||||
"paths": {
|
||||
"react": ["./node_modules/preact/compat/"],
|
||||
"react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
|
||||
|
||||
@@ -8,6 +8,28 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
|
||||
const playground = process.env.PLAYGROUND;
|
||||
|
||||
switch (playground) {
|
||||
case "METRICS": {
|
||||
return {
|
||||
"^/vmalert/.*": {
|
||||
target: "https://play.victoriametrics.com",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
"^/api/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus/",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
case "LOGS": {
|
||||
return {
|
||||
"^/select/.*": {
|
||||
|
||||
@@ -31,6 +31,7 @@ type app struct {
|
||||
binary string
|
||||
flags []string
|
||||
process *os.Process
|
||||
wait bool
|
||||
}
|
||||
|
||||
// appOptions holds the optional configuration of an app, such as default flags
|
||||
@@ -38,6 +39,7 @@ type app struct {
|
||||
type appOptions struct {
|
||||
defaultFlags map[string]string
|
||||
extractREs []*regexp.Regexp
|
||||
wait bool
|
||||
}
|
||||
|
||||
// startApp starts an instance of an app using the app binary file path and
|
||||
@@ -73,6 +75,7 @@ func startApp(instance string, binary string, flags []string, opts *appOptions)
|
||||
binary: binary,
|
||||
flags: flags,
|
||||
process: cmd.Process,
|
||||
wait: opts.wait,
|
||||
}
|
||||
|
||||
go app.processOutput("stdout", stdout, app.writeToStderr)
|
||||
@@ -92,7 +95,11 @@ func startApp(instance string, binary string, flags []string, opts *appOptions)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return app, extracts, nil
|
||||
if app.wait {
|
||||
err = cmd.Wait()
|
||||
}
|
||||
|
||||
return app, extracts, err
|
||||
}
|
||||
|
||||
// setDefaultFlags adds flags with default values to `flags` if it does not
|
||||
@@ -112,9 +119,12 @@ func setDefaultFlags(flags []string, defaultFlags map[string]string) []string {
|
||||
return flags
|
||||
}
|
||||
|
||||
// stop sends the app process a SIGINT signal and waits until it terminates
|
||||
// Stop sends the app process a SIGINT signal and waits until it terminates
|
||||
// gracefully.
|
||||
func (app *app) Stop() {
|
||||
if app.wait {
|
||||
return
|
||||
}
|
||||
if err := app.process.Signal(os.Interrupt); err != nil {
|
||||
log.Fatalf("Could not send SIGINT signal to %s process: %v", app.instance, err)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ type PrometheusQuerier interface {
|
||||
PrometheusAPIV1Query(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
|
||||
PrometheusAPIV1QueryRange(t *testing.T, query string, opts QueryOpts) *PrometheusAPIV1QueryResponse
|
||||
PrometheusAPIV1Series(t *testing.T, matchQuery string, opts QueryOpts) *PrometheusAPIV1SeriesResponse
|
||||
PrometheusAPIV1ExportNative(t *testing.T, query string, opts QueryOpts) []byte
|
||||
}
|
||||
|
||||
// Writer contains methods for writing new data
|
||||
@@ -29,6 +30,7 @@ type Writer interface {
|
||||
PrometheusAPIV1Write(t *testing.T, records []pb.TimeSeries, opts QueryOpts)
|
||||
PrometheusAPIV1ImportPrometheus(t *testing.T, records []string, opts QueryOpts)
|
||||
PrometheusAPIV1ImportCSV(t *testing.T, records []string, opts QueryOpts)
|
||||
PrometheusAPIV1ImportNative(t *testing.T, data []byte, opts QueryOpts)
|
||||
|
||||
// Graphit APIs
|
||||
GraphiteWrite(t *testing.T, records []string, opts QueryOpts)
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
)
|
||||
|
||||
// TestCase holds the state and defines clean-up procedure common for all test
|
||||
@@ -27,6 +28,7 @@ type Stopper interface {
|
||||
|
||||
// NewTestCase creates a new test case.
|
||||
func NewTestCase(t *testing.T) *TestCase {
|
||||
t.Parallel()
|
||||
return &TestCase{t, NewClient(), make(map[string]Stopper)}
|
||||
}
|
||||
|
||||
@@ -189,7 +191,7 @@ func (tc *TestCase) MustStartVmauth(instance string, flags []string, configFileY
|
||||
|
||||
// MustStartDefaultCluster starts a typical cluster configuration with default
|
||||
// flags.
|
||||
func (tc *TestCase) MustStartDefaultCluster() PrometheusWriteQuerier {
|
||||
func (tc *TestCase) MustStartDefaultCluster() *Vmcluster {
|
||||
tc.t.Helper()
|
||||
|
||||
return tc.MustStartCluster(&ClusterOptions{
|
||||
@@ -223,7 +225,7 @@ type ClusterOptions struct {
|
||||
}
|
||||
|
||||
// MustStartCluster starts a typical cluster configuration with custom flags.
|
||||
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) PrometheusWriteQuerier {
|
||||
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) *Vmcluster {
|
||||
tc.t.Helper()
|
||||
|
||||
opts.Vmstorage1Flags = append(opts.Vmstorage1Flags, []string{
|
||||
@@ -251,6 +253,18 @@ func (tc *TestCase) MustStartCluster(opts *ClusterOptions) PrometheusWriteQuerie
|
||||
return &Vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
|
||||
}
|
||||
|
||||
// MustStartVmctl is a test helper function that starts an instance of vmctl
|
||||
func (tc *TestCase) MustStartVmctl(instance string, flags []string) *Vmctl {
|
||||
tc.t.Helper()
|
||||
|
||||
app, err := StartVmctl(instance, flags)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
||||
}
|
||||
tc.addApp(instance, app)
|
||||
return app
|
||||
}
|
||||
|
||||
func (tc *TestCase) addApp(instance string, app Stopper) {
|
||||
if _, alreadyStarted := tc.startedApps[instance]; alreadyStarted {
|
||||
tc.t.Fatalf("%s has already been started", instance)
|
||||
|
||||
73
apptest/tests/export_import_test.go
Normal file
73
apptest/tests/export_import_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
func TestSingleExportImportNative(t *testing.T) {
|
||||
os.RemoveAll(t.Name())
|
||||
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultVmsingle()
|
||||
|
||||
testExportImportNative(tc.T(), sut)
|
||||
}
|
||||
|
||||
func TestClusterExportImportNative(t *testing.T) {
|
||||
os.RemoveAll(t.Name())
|
||||
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultCluster()
|
||||
|
||||
testExportImportNative(tc.T(), sut)
|
||||
}
|
||||
|
||||
// testExportImportNative test export and import in VictoriaMetrics’ native format.
|
||||
// see: https://docs.victoriametrics.com/#how-to-import-data-in-native-format
|
||||
func testExportImportNative(t *testing.T, sut at.PrometheusWriteQuerier) {
|
||||
// create test data
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||||
`native_export_import 10 1707123456700`, // 2024-02-05T08:57:36.700Z
|
||||
}, at.QueryOpts{
|
||||
ExtraLabels: []string{"el1=elv1", "el2=elv2"},
|
||||
})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// export test data via native export API
|
||||
exportResult := sut.PrometheusAPIV1ExportNative(t, "native_export_import", at.QueryOpts{
|
||||
Start: "2024-02-05T08:50:00.700Z",
|
||||
End: "2024-02-05T09:00:00.700Z",
|
||||
})
|
||||
|
||||
// re-import test data via native import API
|
||||
sut.PrometheusAPIV1ImportNative(t, exportResult, at.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
// check query result
|
||||
got := sut.PrometheusAPIV1QueryRange(t, "native_export_import", at.QueryOpts{
|
||||
Start: "2024-02-05T08:57:36.700Z",
|
||||
End: "2024-02-05T08:57:36.700Z",
|
||||
Step: "60s",
|
||||
})
|
||||
|
||||
cmpOptions := []cmp.Option{
|
||||
cmpopts.IgnoreFields(at.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
|
||||
cmpopts.EquateNaNs(),
|
||||
}
|
||||
want := at.NewPrometheusAPIV1QueryResponse(t, `{"data": {"result": [{"metric": {"__name__": "native_export_import", "el1": "elv1", "el2":"elv2"}, "values": []}]}}`)
|
||||
want.Data.Result[0].Samples = []*at.Sample{
|
||||
at.NewSample(t, "2024-02-05T08:57:36.700Z", 10),
|
||||
}
|
||||
if diff := cmp.Diff(want, got, cmpOptions...); diff != "" {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -216,4 +216,15 @@ func TestClusterMultiTenantSelect(t *testing.T) {
|
||||
t.Errorf("unexpected response (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if got := vmselect.GetIntMetric(t, `vm_cache_requests_total{type="multitenancy/tenants"}`); got != 0 {
|
||||
t.Errorf("unexpected multitenancy tenants cache requests; got %d; want 0", got)
|
||||
}
|
||||
|
||||
if got := vmselect.GetIntMetric(t, `vm_cache_misses_total{type="multitenancy/tenants"}`); got != 0 {
|
||||
t.Errorf("unexpected multitenancy tenants cache misses; got %d; want 0", got)
|
||||
}
|
||||
|
||||
if got := vmselect.GetIntMetric(t, `vm_cache_entries{type="multitenancy/tenants"}`); got != 0 {
|
||||
t.Errorf("unexpected multitenancy tenants cache entries; got %d; want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
||||
)
|
||||
|
||||
// snapshotNameRE covers years 1970-2099.
|
||||
@@ -104,7 +105,7 @@ func TestClusterSnapshots_CreateListDelete(t *testing.T) {
|
||||
tc := at.NewTestCase(t)
|
||||
defer tc.Stop()
|
||||
|
||||
sut := tc.MustStartDefaultCluster().(*at.Vmcluster)
|
||||
sut := tc.MustStartDefaultCluster()
|
||||
|
||||
// Insert some data.
|
||||
const numSamples = 1000
|
||||
|
||||
40
apptest/tests/testdata/prometheus/expected_response.json
vendored
Normal file
40
apptest/tests/testdata/prometheus/expected_response.json
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "matrix",
|
||||
"result": [
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "vm_log_messages_total",
|
||||
"job": "victoriametrics",
|
||||
"instance": "victoriametrics:8428",
|
||||
"app_version": "victoria-metrics-20250523-133235-tags-v1.118.0-0-gaa3171cf4b",
|
||||
"level": "info",
|
||||
"location": "VictoriaMetrics/lib/ingestserver/opentsdb/server.go:48"
|
||||
},
|
||||
"values": [
|
||||
[
|
||||
1748897918.112,
|
||||
"1"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "vm_log_messages_total",
|
||||
"job": "victoriametrics",
|
||||
"instance": "victoriametrics:8428",
|
||||
"app_version": "victoria-metrics-20250523-133235-tags-v1.118.0-0-gaa3171cf4b",
|
||||
"level": "info",
|
||||
"location": "VictoriaMetrics/lib/ingestserver/opentsdb/server.go:59"
|
||||
},
|
||||
"values": [
|
||||
[
|
||||
1748897918.112,
|
||||
"1"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user