mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-06-03 17:12:48 +03:00
Compare commits
240 Commits
fix-panel-
...
v1.9.0-vic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa0ac1d0ed | ||
|
|
81d359507d | ||
|
|
ebac07bcf6 | ||
|
|
92bfb7ba15 | ||
|
|
6a8fa799c6 | ||
|
|
5e7866c3a0 | ||
|
|
c26cbf57dd | ||
|
|
ee8f852a83 | ||
|
|
80a5f6863b | ||
|
|
8e576854b3 | ||
|
|
5dbca072bb | ||
|
|
375548ca4b | ||
|
|
f0424facf7 | ||
|
|
48602a1ae8 | ||
|
|
0a6f6d0bba | ||
|
|
171d4019cd | ||
|
|
54f60d804a | ||
|
|
78dc9533fc | ||
|
|
088e06c2c2 | ||
|
|
553e95e293 | ||
|
|
46fbb95b16 | ||
|
|
fbe592f495 | ||
|
|
631b736bc2 | ||
|
|
ceefa406cc | ||
|
|
2748681f40 | ||
|
|
e2cfd351c0 | ||
|
|
a2b49a1843 | ||
|
|
a70e8e0379 | ||
|
|
894a2f91e6 | ||
|
|
30af662d84 | ||
|
|
fa2107bbec | ||
|
|
1427ea3ca9 | ||
|
|
5561970db0 | ||
|
|
4d42e29967 | ||
|
|
72837919ae | ||
|
|
6166149474 | ||
|
|
f34e82eb9b | ||
|
|
2545082c0f | ||
|
|
676669020f | ||
|
|
071db473a7 | ||
|
|
94ac051647 | ||
|
|
3419c56eef | ||
|
|
3091133a36 | ||
|
|
63a1baf10f | ||
|
|
64a3c13c2f | ||
|
|
0b801a147e | ||
|
|
a45afa4cd8 | ||
|
|
552f0ce466 | ||
|
|
f3012c4a85 | ||
|
|
2589065329 | ||
|
|
777f05efcc | ||
|
|
03a1883525 | ||
|
|
17fd53bd37 | ||
|
|
34204cc597 | ||
|
|
e22d47b85c | ||
|
|
0e08a7a125 | ||
|
|
13c4324bb5 | ||
|
|
95f182053b | ||
|
|
ec64a1fd7c | ||
|
|
f61b5da617 | ||
|
|
3c036e0d31 | ||
|
|
aacb9b2726 | ||
|
|
c753f75e18 | ||
|
|
330461c635 | ||
|
|
5a1a28ba87 | ||
|
|
5b18dc0214 | ||
|
|
10abdf34ab | ||
|
|
e55b5fc605 | ||
|
|
3b34241380 | ||
|
|
de144899b2 | ||
|
|
dc9dba71b2 | ||
|
|
fd1568531d | ||
|
|
46bb2d4308 | ||
|
|
8fa8101677 | ||
|
|
204cec7913 | ||
|
|
17988942ab | ||
|
|
ed05ae12c4 | ||
|
|
256924e2d6 | ||
|
|
b5392337bf | ||
|
|
fa4e3607c3 | ||
|
|
043d066133 | ||
|
|
900159a2d3 | ||
|
|
4464c5a254 | ||
|
|
90fed18b83 | ||
|
|
1480ecc129 | ||
|
|
c67f4d4d86 | ||
|
|
c2f5088adc | ||
|
|
87739bbbef | ||
|
|
dce4dc0a33 | ||
|
|
ad6c587494 | ||
|
|
467cdd8a3d | ||
|
|
026894054b | ||
|
|
db9107acef | ||
|
|
f8a0f2fe44 | ||
|
|
bfd83e3cca | ||
|
|
6b20ec9c7d | ||
|
|
f0d55a1c25 | ||
|
|
f31dece58d | ||
|
|
a6951b8b14 | ||
|
|
d56e3df770 | ||
|
|
d88c1fbdbb | ||
|
|
a947ccf228 | ||
|
|
5747e8b5d0 | ||
|
|
aab0174c94 | ||
|
|
2c271aa9b2 | ||
|
|
7cdeb3a32c | ||
|
|
8c4ac815cb | ||
|
|
dcb6dd5dcb | ||
|
|
be24fbe8ae | ||
|
|
fc1a89f51c | ||
|
|
eddeccfcfb | ||
|
|
b620b5cff5 | ||
|
|
42c21ff671 | ||
|
|
b9eb9fe72d | ||
|
|
9ae49b405c | ||
|
|
f932deb47a | ||
|
|
77f446d095 | ||
|
|
5f8810fc8d | ||
|
|
80ead7cfa4 | ||
|
|
8772288bd6 | ||
|
|
338095fdd3 | ||
|
|
299d66fd98 | ||
|
|
77218c5848 | ||
|
|
9e6fc9269d | ||
|
|
661f9fc3e2 | ||
|
|
2adb5fe014 | ||
|
|
ce917a4cc3 | ||
|
|
b3de1c029c | ||
|
|
461c7a5ad7 | ||
|
|
489631b227 | ||
|
|
e78ff0dc2a | ||
|
|
ab4d9f6213 | ||
|
|
81c313fd89 | ||
|
|
e9de665289 | ||
|
|
bfbe06e912 | ||
|
|
71a7d0db4a | ||
|
|
e8748e4747 | ||
|
|
ad3a5be097 | ||
|
|
17b3f24a37 | ||
|
|
1f0b03aebe | ||
|
|
fc8710c071 | ||
|
|
a7f36eef0e | ||
|
|
54ab08d839 | ||
|
|
a3ea6d9e61 | ||
|
|
f19c760f4f | ||
|
|
86e74de9db | ||
|
|
4d4253ee17 | ||
|
|
8c7b5d22c9 | ||
|
|
513f5da5de | ||
|
|
fb4d545555 | ||
|
|
abaf8574a8 | ||
|
|
f346b5aaaa | ||
|
|
31398cc739 | ||
|
|
4574958e2e | ||
|
|
d623105ef4 | ||
|
|
aac5cd8574 | ||
|
|
d3c02b8f5d | ||
|
|
7f252c1800 | ||
|
|
f73b40619a | ||
|
|
0f7b853a88 | ||
|
|
70f0a974b8 | ||
|
|
2eb15cf30c | ||
|
|
499f0b9588 | ||
|
|
43d615ae87 | ||
|
|
82e1c6fc3f | ||
|
|
45bfe1f44c | ||
|
|
58d2c18423 | ||
|
|
feeda42560 | ||
|
|
7d2a6764e7 | ||
|
|
1645542a8a | ||
|
|
151eb1e4b6 | ||
|
|
5e4de8e860 | ||
|
|
6312d3bbba | ||
|
|
d2bede6b51 | ||
|
|
5ca5069fc4 | ||
|
|
8a3c460f63 | ||
|
|
ca653a515c | ||
|
|
e5b4cf33bf | ||
|
|
e24a8f2088 | ||
|
|
f27e120aeb | ||
|
|
ee1ce90501 | ||
|
|
47fe8cf3be | ||
|
|
5813aa6602 | ||
|
|
b4f4ece162 | ||
|
|
bb00f7529f | ||
|
|
ad3bd11334 | ||
|
|
875c6663ef | ||
|
|
b48b7c454a | ||
|
|
f523348b3f | ||
|
|
63bf1e008f | ||
|
|
419ac10c60 | ||
|
|
d631d2c100 | ||
|
|
89431458bf | ||
|
|
d8d0c0ac01 | ||
|
|
c0f5699bad | ||
|
|
277fdd1070 | ||
|
|
d290efb849 | ||
|
|
b26a68641c | ||
|
|
b88cda5c41 | ||
|
|
d2a791bef3 | ||
|
|
99516a5730 | ||
|
|
aecc86c390 | ||
|
|
500b54f5aa | ||
|
|
cc29692e27 | ||
|
|
f018aa33cb | ||
|
|
92b6475fa6 | ||
|
|
bda3546cfd | ||
|
|
2691cdefe3 | ||
|
|
93b8aa5c9d | ||
|
|
7a7f188133 | ||
|
|
3e00fae3f4 | ||
|
|
ee3c0c6a87 | ||
|
|
cf7ea78588 | ||
|
|
186aa3bb0e | ||
|
|
e368f687a7 | ||
|
|
0214aa328e | ||
|
|
dd919eeee6 | ||
|
|
3f22d06b0c | ||
|
|
b812de236b | ||
|
|
40f56fa93b | ||
|
|
e610edf045 | ||
|
|
764955b61c | ||
|
|
e3d31a371a | ||
|
|
df723a4870 | ||
|
|
bd00e3a735 | ||
|
|
e794582f31 | ||
|
|
7cab4fd30d | ||
|
|
3333135bc0 | ||
|
|
1db1841b20 | ||
|
|
f7ce191482 | ||
|
|
96ea222780 | ||
|
|
03c0d9a672 | ||
|
|
e9f86af7f5 | ||
|
|
9ada784983 | ||
|
|
a83ee2b3f1 | ||
|
|
2564f10d98 | ||
|
|
0871770634 | ||
|
|
51b21dfd57 | ||
|
|
276989716f | ||
|
|
6cb3c0cac8 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
restore-keys: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-
|
||||
|
||||
- name: Run tests
|
||||
run: make ${{ matrix.scenario}}
|
||||
run: GOGC=10 make ${{ matrix.scenario}}
|
||||
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -175,7 +175,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2019-2024 VictoriaMetrics, Inc.
|
||||
Copyright 2019-2025 VictoriaMetrics, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
12
Makefile
12
Makefile
@@ -513,19 +513,19 @@ check-all: fmt vet golangci-lint govulncheck
|
||||
clean-checkers: remove-golangci-lint remove-govulncheck
|
||||
|
||||
test:
|
||||
DISABLE_FSYNC_FOR_TESTING=1 go test ./lib/... ./app/...
|
||||
go test ./lib/... ./app/...
|
||||
|
||||
test-race:
|
||||
DISABLE_FSYNC_FOR_TESTING=1 go test -race ./lib/... ./app/...
|
||||
go test -race ./lib/... ./app/...
|
||||
|
||||
test-pure:
|
||||
DISABLE_FSYNC_FOR_TESTING=1 CGO_ENABLED=0 go test ./lib/... ./app/...
|
||||
CGO_ENABLED=0 go test ./lib/... ./app/...
|
||||
|
||||
test-full:
|
||||
DISABLE_FSYNC_FOR_TESTING=1 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
|
||||
test-full-386:
|
||||
DISABLE_FSYNC_FOR_TESTING=1 GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
|
||||
|
||||
integration-test: victoria-metrics vmagent vmalert vmauth
|
||||
go test ./apptest/... -skip="^TestCluster.*"
|
||||
@@ -567,7 +567,7 @@ golangci-lint: install-golangci-lint
|
||||
golangci-lint run
|
||||
|
||||
install-golangci-lint:
|
||||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.60.3
|
||||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.63.4
|
||||
|
||||
remove-golangci-lint:
|
||||
rm -rf `which golangci-lint`
|
||||
|
||||
@@ -42,6 +42,10 @@ var (
|
||||
"Smaller intervals increase disk IO load. Minimum supported value is 1s")
|
||||
maxIngestionRate = flag.Int("maxIngestionRate", 0, "The maximum number of samples vmsingle can receive per second. Data ingestion is paused when the limit is exceeded. "+
|
||||
"By default there are no limits on samples ingestion rate.")
|
||||
finalDedupScheduleInterval = flag.Duration("storage.finalDedupScheduleCheckInterval", time.Hour, "The interval for checking when final deduplication process should be started."+
|
||||
"Storage unconditionally adds 25% jitter to the interval value on each check evaluation."+
|
||||
" Changing the interval to the bigger values may delay downsampling, deduplication for historical data."+
|
||||
" See also https://docs.victoriametrics.com/#deduplication")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -86,6 +90,10 @@ func main() {
|
||||
startTime := time.Now()
|
||||
storage.SetDedupInterval(*minScrapeInterval)
|
||||
storage.SetDataFlushInterval(*inmemoryDataFlushInterval)
|
||||
if *finalDedupScheduleInterval < time.Hour {
|
||||
logger.Fatalf("-dedup.finalDedupScheduleCheckInterval cannot be smaller than 1 hour; got %s", *finalDedupScheduleInterval)
|
||||
}
|
||||
storage.SetFinalDedupScheduleInterval(*finalDedupScheduleInterval)
|
||||
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
|
||||
vmselect.Init()
|
||||
vminsertcommon.StartIngestionRateLimiter(*maxIngestionRate)
|
||||
|
||||
@@ -199,8 +199,8 @@ func (lmp *logMessageProcessor) AddRow(timestamp int64, fields, streamFields []l
|
||||
lmp.bytesIngestedTotal.Add(n)
|
||||
|
||||
if len(fields) > *MaxFieldsPerLine {
|
||||
rf := logstorage.RowFormatter(fields)
|
||||
logger.Warnf("dropping log line with %d fields; it exceeds -insert.maxFieldsPerLine=%d; %s", len(fields), *MaxFieldsPerLine, rf)
|
||||
line := logstorage.MarshalFieldsToJSON(nil, fields)
|
||||
logger.Warnf("dropping log line with %d fields; it exceeds -insert.maxFieldsPerLine=%d; %s", len(fields), *MaxFieldsPerLine, line)
|
||||
rowsDroppedTotalTooManyFields.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
|
||||
var (
|
||||
// MaxLineSizeBytes is the maximum length of a single line for /insert/* handlers
|
||||
MaxLineSizeBytes = flagutil.NewBytes("insert.maxLineSizeBytes", 256*1024, "The maximum size of a single line, which can be read by /insert/* handlers")
|
||||
MaxLineSizeBytes = flagutil.NewBytes("insert.maxLineSizeBytes", 256*1024, "The maximum size of a single line, which can be read by /insert/* handlers; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/faq/#what-length-a-log-record-is-expected-to-have")
|
||||
|
||||
// MaxFieldsPerLine is the maximum number of fields per line for /insert/* handlers
|
||||
MaxFieldsPerLine = flag.Int("insert.maxFieldsPerLine", 1000, "The maximum number of log fields per line, which can be read by /insert/* handlers")
|
||||
MaxFieldsPerLine = flag.Int("insert.maxFieldsPerLine", 1000, "The maximum number of log fields per line, which can be read by /insert/* handlers; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/faq/#how-many-fields-a-single-log-entry-may-contain")
|
||||
)
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
// LineReader reads newline-delimited lines from the underlying reader
|
||||
type LineReader struct {
|
||||
// Line contains the next line read after the call to NextLine
|
||||
//
|
||||
// The Line contents is valid until the next call to NextLine.
|
||||
Line []byte
|
||||
|
||||
// name is the LineReader name
|
||||
@@ -26,6 +28,9 @@ type LineReader struct {
|
||||
// buf is a buffer for reading the next line
|
||||
buf []byte
|
||||
|
||||
// bufOffset is the offset at buf to read the next line from
|
||||
bufOffset int
|
||||
|
||||
// err is the last error when reading data from r
|
||||
err error
|
||||
|
||||
@@ -51,26 +56,27 @@ func NewLineReader(name string, r io.Reader) *LineReader {
|
||||
// Check for Err in this case.
|
||||
func (lr *LineReader) NextLine() bool {
|
||||
for {
|
||||
if len(lr.buf) == 0 {
|
||||
if lr.bufOffset >= len(lr.buf) {
|
||||
if lr.err != nil || lr.eofReached {
|
||||
return false
|
||||
}
|
||||
if !lr.readMoreData() {
|
||||
return false
|
||||
}
|
||||
if len(lr.buf) == 0 && lr.eofReached {
|
||||
if lr.bufOffset >= len(lr.buf) && lr.eofReached {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if n := bytes.IndexByte(lr.buf, '\n'); n >= 0 {
|
||||
lr.Line = append(lr.Line[:0], lr.buf[:n]...)
|
||||
lr.buf = append(lr.buf[:0], lr.buf[n+1:]...)
|
||||
buf := lr.buf[lr.bufOffset:]
|
||||
if n := bytes.IndexByte(buf, '\n'); n >= 0 {
|
||||
lr.Line = buf[:n]
|
||||
lr.bufOffset += n + 1
|
||||
return true
|
||||
}
|
||||
if lr.eofReached {
|
||||
lr.Line = append(lr.Line[:0], lr.buf...)
|
||||
lr.buf = lr.buf[:0]
|
||||
lr.Line = buf
|
||||
lr.bufOffset += len(buf)
|
||||
return true
|
||||
}
|
||||
if !lr.readMoreData() {
|
||||
@@ -88,6 +94,11 @@ func (lr *LineReader) Err() error {
|
||||
}
|
||||
|
||||
func (lr *LineReader) readMoreData() bool {
|
||||
if lr.bufOffset > 0 {
|
||||
lr.buf = append(lr.buf[:0], lr.buf[lr.bufOffset:]...)
|
||||
lr.bufOffset = 0
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -2,20 +2,19 @@ package insertutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
)
|
||||
|
||||
// ExtractTimestampRFC3339NanoFromFields extracts RFC3339 timestamp in nanoseconds from the field with the name timeField at fields.
|
||||
// ExtractTimestampFromFields extracts timestamp in nanoseconds from the field with the name timeField at fields.
|
||||
//
|
||||
// The value for the timeField is set to empty string after returning from the function,
|
||||
// so it could be ignored during data ingestion.
|
||||
//
|
||||
// The current timestamp is returned if fields do not contain a field with timeField name or if the timeField value is empty.
|
||||
func ExtractTimestampRFC3339NanoFromFields(timeField string, fields []logstorage.Field) (int64, error) {
|
||||
func ExtractTimestampFromFields(timeField string, fields []logstorage.Field) (int64, error) {
|
||||
for i := range fields {
|
||||
f := &fields[i]
|
||||
if f.Name != timeField {
|
||||
@@ -48,22 +47,24 @@ func parseTimestamp(s string) (int64, error) {
|
||||
return nsecs, nil
|
||||
}
|
||||
|
||||
// ParseUnixTimestamp parses s as unix timestamp in either seconds or milliseconds and returns the parsed timestamp in nanoseconds.
|
||||
// ParseUnixTimestamp parses s as unix timestamp in seconds, milliseconds, microseconds or nanoseconds and returns the parsed timestamp in nanoseconds.
|
||||
func ParseUnixTimestamp(s string) (int64, error) {
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse unix timestamp from %q: %w", s, err)
|
||||
}
|
||||
if n < (1<<31) && n >= (-1<<31) {
|
||||
// The timestamp is in seconds. Convert it to milliseconds
|
||||
n *= 1e3
|
||||
// The timestamp is in seconds.
|
||||
return n * 1e9, nil
|
||||
}
|
||||
if n > int64(math.MaxInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too big timestamp in milliseconds: %d; mustn't exceed %d", n, int64(math.MaxInt64)/1e6)
|
||||
if n < 1e3*(1<<31) && n >= 1e3*(-1<<31) {
|
||||
// The timestamp is in milliseconds.
|
||||
return n * 1e6, nil
|
||||
}
|
||||
if n < int64(math.MinInt64)/1e6 {
|
||||
return 0, fmt.Errorf("too small timestamp in milliseconds: %d; must be bigger than %d", n, int64(math.MinInt64)/1e6)
|
||||
if n < 1e6*(1<<31) && n >= 1e6*(-1<<31) {
|
||||
// The timestamp is in microseconds.
|
||||
return n * 1e3, nil
|
||||
}
|
||||
n *= 1e6
|
||||
// The timestamp is in nanoseconds
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
|
||||
)
|
||||
|
||||
func TestExtractTimestampRFC3339NanoFromFields_Success(t *testing.T) {
|
||||
func TestExtractTimestampFromFields_Success(t *testing.T) {
|
||||
f := func(timeField string, fields []logstorage.Field, nsecsExpected int64) {
|
||||
t.Helper()
|
||||
|
||||
nsecs, err := ExtractTimestampRFC3339NanoFromFields(timeField, fields)
|
||||
nsecs, err := ExtractTimestampFromFields(timeField, fields)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
@@ -51,6 +51,18 @@ func TestExtractTimestampRFC3339NanoFromFields_Success(t *testing.T) {
|
||||
{Name: "foo", Value: "bar"},
|
||||
}, 1718773640123456789)
|
||||
|
||||
// Unix timestamp in nanoseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "1718773640123456789"},
|
||||
}, 1718773640123456789)
|
||||
|
||||
// Unix timestamp in microseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
{Name: "time", Value: "1718773640123456"},
|
||||
}, 1718773640123456000)
|
||||
|
||||
// Unix timestamp in milliseconds
|
||||
f("time", []logstorage.Field{
|
||||
{Name: "foo", Value: "bar"},
|
||||
@@ -64,14 +76,14 @@ func TestExtractTimestampRFC3339NanoFromFields_Success(t *testing.T) {
|
||||
}, 1718773640000000000)
|
||||
}
|
||||
|
||||
func TestExtractTimestampRFC3339NanoFromFields_Error(t *testing.T) {
|
||||
func TestExtractTimestampFromFields_Error(t *testing.T) {
|
||||
f := func(s string) {
|
||||
t.Helper()
|
||||
|
||||
fields := []logstorage.Field{
|
||||
{Name: "time", Value: s},
|
||||
}
|
||||
nsecs, err := ExtractTimestampRFC3339NanoFromFields("time", fields)
|
||||
nsecs, err := ExtractTimestampFromFields("time", fields)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
@@ -80,6 +92,7 @@ func TestExtractTimestampRFC3339NanoFromFields_Error(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// invalid time
|
||||
f("foobar")
|
||||
|
||||
// incomplete time
|
||||
|
||||
@@ -51,20 +51,13 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
lmp := cp.NewLogMessageProcessor("jsonline")
|
||||
streamName := fmt.Sprintf("remoteAddr=%s, requestURI=%q", httpserver.GetQuotedRemoteAddr(r), r.RequestURI)
|
||||
err = processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
|
||||
processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
|
||||
lmp.MustClose()
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("jsonline: %s", err)
|
||||
} else {
|
||||
// update requestDuration only for successfully parsed requests.
|
||||
// There is no need in updating requestDuration for request errors,
|
||||
// since their timings are usually much smaller than the timing for successful request parsing.
|
||||
requestDuration.UpdateDuration(startTime)
|
||||
}
|
||||
requestDuration.UpdateDuration(startTime)
|
||||
}
|
||||
|
||||
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) error {
|
||||
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) {
|
||||
wcr := writeconcurrencylimiter.GetReader(r)
|
||||
defer writeconcurrencylimiter.PutReader(wcr)
|
||||
|
||||
@@ -76,10 +69,10 @@ func processStreamInternal(streamName string, r io.Reader, timeField string, msg
|
||||
wcr.DecConcurrency()
|
||||
if err != nil {
|
||||
errorsTotal.Inc()
|
||||
return fmt.Errorf("cannot read line #%d in /jsonline request: %s", n, err)
|
||||
logger.Warnf("jsonline: cannot read line #%d in /jsonline request: %s", n, err)
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
n++
|
||||
}
|
||||
@@ -96,16 +89,17 @@ func readLine(lr *insertutils.LineReader, timeField string, msgFields []string,
|
||||
}
|
||||
|
||||
p := logstorage.GetJSONParser()
|
||||
defer logstorage.PutJSONParser(p)
|
||||
|
||||
if err := p.ParseLogMessage(line); err != nil {
|
||||
return false, fmt.Errorf("cannot parse json-encoded log entry: %w", err)
|
||||
return true, fmt.Errorf("cannot parse json-encoded line: %w; line contents: %q", err, line)
|
||||
}
|
||||
ts, err := insertutils.ExtractTimestampRFC3339NanoFromFields(timeField, p.Fields)
|
||||
ts, err := insertutils.ExtractTimestampFromFields(timeField, p.Fields)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot get timestamp: %w", err)
|
||||
return true, fmt.Errorf("cannot get timestamp from json-encoded line: %w; line contents: %q", err, line)
|
||||
}
|
||||
logstorage.RenameField(p.Fields, msgFields, "_msg")
|
||||
lmp.AddRow(ts, p.Fields, nil)
|
||||
logstorage.PutJSONParser(p)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -7,16 +7,14 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutils"
|
||||
)
|
||||
|
||||
func TestProcessStreamInternal_Success(t *testing.T) {
|
||||
func TestProcessStreamInternal(t *testing.T) {
|
||||
f := func(data, timeField, msgField string, timestampsExpected []int64, resultExpected string) {
|
||||
t.Helper()
|
||||
|
||||
msgFields := []string{msgField}
|
||||
tlp := &insertutils.TestLogMessageProcessor{}
|
||||
r := bytes.NewBufferString(data)
|
||||
if err := processStreamInternal("test", r, timeField, msgFields, tlp); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
processStreamInternal("test", r, timeField, msgFields, tlp)
|
||||
|
||||
if err := tlp.Verify(timestampsExpected, resultExpected); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -45,22 +43,37 @@ func TestProcessStreamInternal_Success(t *testing.T) {
|
||||
resultExpected = `{"log.offset":"71770","log.file.path":"/var/log/auth.log","message":"foobar"}
|
||||
{"message":"baz"}`
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
}
|
||||
|
||||
func TestProcessStreamInternal_Failure(t *testing.T) {
|
||||
f := func(data string) {
|
||||
t.Helper()
|
||||
|
||||
tlp := &insertutils.TestLogMessageProcessor{}
|
||||
r := bytes.NewBufferString(data)
|
||||
if err := processStreamInternal("test", r, "time", nil, tlp); err == nil {
|
||||
t.Fatalf("expecting non-nil error")
|
||||
}
|
||||
}
|
||||
|
||||
// invalid json
|
||||
f("foobar")
|
||||
data = "foobar"
|
||||
timeField = "@timestamp"
|
||||
msgField = "aaa"
|
||||
timestampsExpected = nil
|
||||
resultExpected = ``
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
|
||||
// invalid timestamp field
|
||||
f(`{"time":"foobar"}`)
|
||||
data = `{"time":"foobar"}`
|
||||
timeField = "time"
|
||||
msgField = "abc"
|
||||
timestampsExpected = nil
|
||||
resultExpected = ``
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
|
||||
// invalid lines among valid lines
|
||||
data = `
|
||||
dsfodmasd
|
||||
|
||||
{"time":"2023-06-06T04:48:11.735Z","log":{"offset":71770,"file":{"path":"/var/log/auth.log"}},"message":"foobar"}
|
||||
invalid line
|
||||
{"time":"2023-06-06T04:48:12.735+01:00","message":"baz"}
|
||||
asbsdf
|
||||
|
||||
`
|
||||
timeField = "time"
|
||||
msgField = "message"
|
||||
timestampsExpected = []int64{1686026891735000000, 1686023292735000000}
|
||||
resultExpected = `{"log.offset":"71770","log.file.path":"/var/log/auth.log","_msg":"foobar"}
|
||||
{"_msg":"baz"}`
|
||||
f(data, timeField, msgField, timestampsExpected, resultExpected)
|
||||
}
|
||||
|
||||
@@ -560,7 +560,7 @@ func processLine(line []byte, currentYear int, timezone *time.Location, useLocal
|
||||
if useLocalTimestamp {
|
||||
ts = time.Now().UnixNano()
|
||||
} else {
|
||||
nsecs, err := insertutils.ExtractTimestampRFC3339NanoFromFields("timestamp", p.Fields)
|
||||
nsecs, err := insertutils.ExtractTimestampFromFields("timestamp", p.Fields)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get timestamp from syslog line %q: %w", line, err)
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ func writeCompactObject(w io.Writer, fields []logstorage.Field) error {
|
||||
_, err := fmt.Fprintf(w, "%s\n", fields[0].Value)
|
||||
return err
|
||||
}
|
||||
if len(fields) == 2 && fields[0].Name == "_time" || fields[1].Name == "_time" {
|
||||
if len(fields) == 2 && (fields[0].Name == "_time" || fields[1].Name == "_time") {
|
||||
// Write _time\tfieldValue as is
|
||||
if fields[0].Name == "_time" {
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\n", fields[0].Value, fields[1].Value)
|
||||
|
||||
@@ -270,7 +270,7 @@ func printCommandsHelp(w io.Writer) {
|
||||
\h - show this help
|
||||
\s - singleline json output mode
|
||||
\m - multiline json output mode
|
||||
\c - compact output
|
||||
\c - compact output mode
|
||||
\logfmt - logfmt output mode
|
||||
\wrap_long_lines - toggles wrapping long lines
|
||||
\tail <query> - live tail <query> results
|
||||
|
||||
@@ -45,6 +45,8 @@ var (
|
||||
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
|
||||
u64FieldsPerLog = flag.Int("u64FieldsPerLog", 1, "The number of fields with uint64 values to generate per each log entry; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
|
||||
i64FieldsPerLog = flag.Int("i64FieldsPerLog", 1, "The number of fields with int64 values to generate per each log entry; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
|
||||
floatFieldsPerLog = flag.Int("floatFieldsPerLog", 1, "The number of fields with float64 values to generate per each log entry; "+
|
||||
"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
|
||||
ipFieldsPerLog = flag.Int("ipFieldsPerLog", 1, "The number of fields with IPv4 values to generate per each log entry; "+
|
||||
@@ -254,6 +256,9 @@ func generateLogsAtTimestamp(bw *bufio.Writer, workerID int, ts int64, firstStre
|
||||
for j := 0; j < *u64FieldsPerLog; j++ {
|
||||
fmt.Fprintf(bw, `,"u64_%d":"%d"`, j, rand.Uint64())
|
||||
}
|
||||
for j := 0; j < *i64FieldsPerLog; j++ {
|
||||
fmt.Fprintf(bw, `,"i64_%d":"%d"`, j, int64(rand.Uint64()))
|
||||
}
|
||||
for j := 0; j < *floatFieldsPerLog; j++ {
|
||||
fmt.Fprintf(bw, `,"float_%d":"%v"`, j, math.Round(10_000*rand.Float64())/1000)
|
||||
}
|
||||
|
||||
@@ -688,13 +688,13 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
|
||||
m := make(map[string]*statsSeries)
|
||||
var mLock sync.Mutex
|
||||
|
||||
timestamp := q.GetTimestamp()
|
||||
writeBlock := func(_ uint, timestamps []int64, columns []logstorage.BlockColumn) {
|
||||
clonedColumnNames := make([]string, len(columns))
|
||||
for i, c := range columns {
|
||||
clonedColumnNames[i] = strings.Clone(c.Name)
|
||||
}
|
||||
for i := range timestamps {
|
||||
timestamp := q.GetTimestamp()
|
||||
labels := make([]logstorage.Field, 0, len(byFields))
|
||||
for j, c := range columns {
|
||||
if c.Name == "_time" {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestParseExtraFilters_Success(t *testing.T) {
|
||||
// LogsQL filter
|
||||
f(`foobar`, `foobar`)
|
||||
f(`foo:bar`, `foo:bar`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
|
||||
}
|
||||
|
||||
func TestParseExtraFilters_Failure(t *testing.T) {
|
||||
@@ -77,7 +77,7 @@ func TestParseExtraStreamFilters_Success(t *testing.T) {
|
||||
// LogsQL filter
|
||||
f(`foobar`, `foobar`)
|
||||
f(`foo:bar`, `foo:bar`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
|
||||
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `{foo="bar",baz="z"} (foo:bar or foo:baz) error _time:5m`)
|
||||
}
|
||||
|
||||
func TestParseExtraStreamFilters_Failure(t *testing.T) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.fa83344e.css",
|
||||
"main.js": "./static/js/main.8ad2bc1f.js",
|
||||
"main.css": "./static/css/main.02a1c6cb.css",
|
||||
"main.js": "./static/js/main.55c8060b.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.fa83344e.css",
|
||||
"static/js/main.8ad2bc1f.js"
|
||||
"static/css/main.02a1c6cb.css",
|
||||
"static/js/main.55c8060b.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.8ad2bc1f.js"></script><link href="./static/css/main.fa83344e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore your log data with VictoriaLogs UI"/><link rel="manifest" href="./manifest.json"/><title>UI for VictoriaLogs</title><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaLogs"><meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/"><meta name="twitter:description" content="Explore your log data with VictoriaLogs UI"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><meta property="og:title" content="UI for VictoriaLogs"><meta property="og:url" content="https://victoriametrics.com/products/victorialogs/"><meta property="og:description" content="Explore your log data with VictoriaLogs UI"><script defer="defer" src="./static/js/main.55c8060b.js"></script><link href="./static/css/main.02a1c6cb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
1
app/vlselect/vmui/static/css/main.02a1c6cb.css
Normal file
1
app/vlselect/vmui/static/css/main.02a1c6cb.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
2
app/vlselect/vmui/static/js/main.55c8060b.js
Normal file
2
app/vlselect/vmui/static/js/main.55c8060b.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
File diff suppressed because it is too large
Load Diff
@@ -160,8 +160,8 @@ func (m *manager) update(ctx context.Context, groupsCfg []config.Group, restore
|
||||
// it is important to call InterruptEval before the update, because cancel fn
|
||||
// can be re-assigned during the update.
|
||||
item.old.InterruptEval()
|
||||
go func(old *rule.Group, new *rule.Group) {
|
||||
old.UpdateWith(new)
|
||||
go func(oldGroup *rule.Group, newGroup *rule.Group) {
|
||||
oldGroup.UpdateWith(newGroup)
|
||||
wg.Done()
|
||||
}(item.old, item.new)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
)
|
||||
|
||||
@@ -69,7 +70,17 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
|
||||
|
||||
func (am *AlertManager) send(ctx context.Context, alerts []Alert, headers map[string]string) error {
|
||||
b := &bytes.Buffer{}
|
||||
writeamRequest(b, alerts, am.argFunc, am.relabelConfigs)
|
||||
alertsToSend := alerts[:0]
|
||||
lblss := make([][]prompbmarshal.Label, 0, len(alerts))
|
||||
for _, a := range alerts {
|
||||
lbls := a.applyRelabelingIfNeeded(am.relabelConfigs)
|
||||
if len(lbls) == 0 {
|
||||
continue
|
||||
}
|
||||
alertsToSend = append(alertsToSend, a)
|
||||
lblss = append(lblss, lbls)
|
||||
}
|
||||
writeamRequest(b, alertsToSend, am.argFunc, lblss)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, am.addr.String(), b)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
{% import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
) %}
|
||||
{% stripspace %}
|
||||
|
||||
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) %}
|
||||
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) %}
|
||||
[
|
||||
{% for i, alert := range alerts %}
|
||||
{% code lbls := alert.applyRelabelingIfNeeded(relabelCfg) %}
|
||||
{% if len(lbls) == 0 %} {% continue %} {% endif %}
|
||||
{% code lbls := lblss[i] %}
|
||||
{
|
||||
"startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %},
|
||||
"generatorURL": {%q= generatorURL(alert) %},
|
||||
|
||||
@@ -8,7 +8,7 @@ package notifier
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
|
||||
)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:8
|
||||
@@ -25,122 +25,116 @@ var (
|
||||
)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:8
|
||||
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) {
|
||||
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:8
|
||||
qw422016.N().S(`[`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:10
|
||||
for i, alert := range alerts {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:11
|
||||
lbls := alert.applyRelabelingIfNeeded(relabelCfg)
|
||||
lbls := lblss[i]
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
if len(lbls) == 0 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
continue
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:12
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:11
|
||||
qw422016.N().S(`{"startsAt":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:13
|
||||
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:13
|
||||
qw422016.N().S(`,"generatorURL":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
qw422016.N().Q(generatorURL(alert))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:14
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
if !alert.End.IsZero() {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:15
|
||||
qw422016.N().S(`"endsAt":`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:17
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:17
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:16
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:18
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:17
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:18
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:17
|
||||
qw422016.N().S(`"labels": {`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:20
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:19
|
||||
ll := len(lbls)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:20
|
||||
for idx, l := range lbls {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
qw422016.N().Q(l.Name)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
qw422016.N().Q(l.Value)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
if idx != ll-1 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:21
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:23
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:23
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:22
|
||||
qw422016.N().S(`},"annotations": {`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:26
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:25
|
||||
c := len(alert.Annotations)
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:27
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:26
|
||||
for k, v := range alert.Annotations {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:27
|
||||
c = c - 1
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
qw422016.N().Q(k)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
qw422016.N().S(`:`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
qw422016.N().Q(v)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
if c > 0 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:28
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:30
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:30
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:29
|
||||
qw422016.N().S(`}}`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:32
|
||||
if i != len(alerts)-1 {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:32
|
||||
qw422016.N().S(`,`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:32
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:34
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
}
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:34
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:33
|
||||
qw422016.N().S(`]`)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
}
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
streamamRequest(qw422016, alerts, generatorURL, relabelCfg)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
streamamRequest(qw422016, alerts, generatorURL, lblss)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
}
|
||||
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) string {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
func amRequest(alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) string {
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
writeamRequest(qb422016, alerts, generatorURL, relabelCfg)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
writeamRequest(qb422016, alerts, generatorURL, lblss)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
qs422016 := string(qb422016.B)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
return qs422016
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:36
|
||||
//line app/vmalert/notifier/alertmanager_request.qtpl:35
|
||||
}
|
||||
|
||||
@@ -105,6 +105,16 @@ func TestAlertManager_Send(t *testing.T) {
|
||||
if r.Header.Get(headerKey) != "bar" {
|
||||
t.Fatalf("expected header %q to be set to %q; got %q instead", headerKey, "bar", r.Header.Get(headerKey))
|
||||
}
|
||||
case 4:
|
||||
var a []struct {
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
||||
t.Fatalf("can not unmarshal data into alert %s", err)
|
||||
}
|
||||
if len(a) != 1 {
|
||||
t.Fatalf("expected 1 alert in array got %d", len(a))
|
||||
}
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
@@ -168,7 +178,20 @@ func TestAlertManager_Send(t *testing.T) {
|
||||
t.Fatalf("unexpected error %s", err)
|
||||
}
|
||||
|
||||
if c != 3 {
|
||||
t.Fatalf("expected 3 calls(count from zero) to server got %d", c)
|
||||
if err := am.Send(context.Background(), []Alert{
|
||||
{
|
||||
Name: "alert1",
|
||||
Labels: map[string]string{"rule": "test"},
|
||||
},
|
||||
{
|
||||
Name: "alert2",
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}, map[string]string{}); err != nil {
|
||||
t.Fatalf("unexpected error %s", err)
|
||||
}
|
||||
|
||||
if c != 4 {
|
||||
t.Fatalf("expected 4 calls(count from zero) to server got %d", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
|
||||
maxQueueSize = flag.Int("remoteWrite.maxQueueSize", defaultMaxQueueSize, "Defines the max number of pending datapoints to remote write endpoint")
|
||||
maxBatchSize = flag.Int("remoteWrite.maxBatchSize", defaultMaxBatchSize, "Defines max number of timeseries to be flushed at once")
|
||||
concurrency = flag.Int("remoteWrite.concurrency", defaultConcurrency, "Defines number of writers for concurrent writing into remote write endpoint")
|
||||
concurrency = flag.Int("remoteWrite.concurrency", defaultConcurrency, "Defines number of writers for concurrent writing into remote write endpoint. Default value depends on the number of available CPU cores.")
|
||||
flushInterval = flag.Duration("remoteWrite.flushInterval", defaultFlushInterval, "Defines interval of flushes to remote write endpoint")
|
||||
|
||||
tlsInsecureSkipVerify = flag.Bool("remoteWrite.tlsInsecureSkipVerify", false, "Whether to skip tls verification when connecting to -remoteWrite.url")
|
||||
|
||||
@@ -443,8 +443,8 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
|
||||
}
|
||||
|
||||
// UpdateWith inserts new group to updateCh
|
||||
func (g *Group) UpdateWith(new *Group) {
|
||||
g.updateCh <- new
|
||||
func (g *Group) UpdateWith(newGroup *Group) {
|
||||
g.updateCh <- newGroup
|
||||
}
|
||||
|
||||
// DeepCopy returns a deep copy of group
|
||||
|
||||
@@ -31,7 +31,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", "TCP address to listen for incoming http requests. See also -tls and -httpListenAddr.useProxyProtocol")
|
||||
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", "TCP address to listen for incoming http requests. "+
|
||||
"By default, serves internal API and proxy requests. "+
|
||||
" See also -tls, -httpListenAddr.useProxyProtocol and -httpInternalListenAddr.")
|
||||
httpInternalListenAddr = flagutil.NewArrayString("httpInternalListenAddr", "TCP address to listen for incoming internal API http requests. Such as /health, /-/reload, /debug/pprof, etc. "+
|
||||
"If flag is set, vmauth no longer serves internal API at -httpListenAddr.")
|
||||
useProxyProtocol = flagutil.NewArrayBool("httpListenAddr.useProxyProtocol", "Whether to use proxy protocol for connections accepted at the corresponding -httpListenAddr . "+
|
||||
"See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt . "+
|
||||
"With enabled proxy protocol http server cannot serve regular /metrics endpoint. Use -pushmetrics.url for metrics pushing")
|
||||
@@ -91,7 +95,21 @@ func main() {
|
||||
logger.Infof("starting vmauth at %q...", listenAddrs)
|
||||
startTime := time.Now()
|
||||
initAuthConfig()
|
||||
go httpserver.Serve(listenAddrs, useProxyProtocol, requestHandler)
|
||||
disableInternalRoutes := len(*httpInternalListenAddr) > 0
|
||||
rh := requestHandlerWithInternalRoutes
|
||||
if disableInternalRoutes {
|
||||
rh = requestHandler
|
||||
}
|
||||
|
||||
serveOpts := httpserver.ServeOptions{
|
||||
UseProxyProtocol: useProxyProtocol,
|
||||
DisableBuiltinRoutes: disableInternalRoutes,
|
||||
}
|
||||
go httpserver.ServeWithOpts(listenAddrs, rh, serveOpts)
|
||||
|
||||
if len(*httpInternalListenAddr) > 0 {
|
||||
go httpserver.Serve(*httpInternalListenAddr, nil, internalRequestHandler)
|
||||
}
|
||||
logger.Infof("started vmauth in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
pushmetrics.Init()
|
||||
@@ -109,7 +127,7 @@ func main() {
|
||||
logger.Infof("successfully stopped vmauth in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
func internalRequestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
switch r.URL.Path {
|
||||
case "/-/reload":
|
||||
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
|
||||
@@ -120,6 +138,17 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requestHandlerWithInternalRoutes(w http.ResponseWriter, r *http.Request) bool {
|
||||
if internalRequestHandler(w, r) {
|
||||
return true
|
||||
}
|
||||
return requestHandler(w, r)
|
||||
}
|
||||
|
||||
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
ats := getAuthTokensFromRequest(r)
|
||||
if len(ats) == 0 {
|
||||
@@ -222,8 +251,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
|
||||
isDefault = true
|
||||
}
|
||||
|
||||
rtb := getReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
|
||||
defer putReadTrackingBody(rtb)
|
||||
rtb := newReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
|
||||
r.Body = rtb
|
||||
|
||||
maxAttempts := up.getBackendsCount()
|
||||
@@ -559,22 +587,11 @@ type readTrackingBody struct {
|
||||
bufComplete bool
|
||||
}
|
||||
|
||||
func (rtb *readTrackingBody) reset() {
|
||||
rtb.maxBodySize = 0
|
||||
rtb.r = nil
|
||||
rtb.buf = rtb.buf[:0]
|
||||
rtb.readBuf = nil
|
||||
rtb.cannotRetry = false
|
||||
rtb.bufComplete = false
|
||||
}
|
||||
|
||||
func getReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
|
||||
v := readTrackingBodyPool.Get()
|
||||
if v == nil {
|
||||
v = &readTrackingBody{}
|
||||
}
|
||||
rtb := v.(*readTrackingBody)
|
||||
|
||||
func newReadTrackingBody(r io.ReadCloser, maxBodySize int) *readTrackingBody {
|
||||
// do not use sync.Pool there
|
||||
// since http.RoundTrip may still use request body after return
|
||||
// See this issue for details https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8051
|
||||
rtb := &readTrackingBody{}
|
||||
if maxBodySize < 0 {
|
||||
maxBodySize = 0
|
||||
}
|
||||
@@ -597,13 +614,6 @@ func (r *zeroReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func putReadTrackingBody(rtb *readTrackingBody) {
|
||||
rtb.reset()
|
||||
readTrackingBodyPool.Put(rtb)
|
||||
}
|
||||
|
||||
var readTrackingBodyPool sync.Pool
|
||||
|
||||
// Read implements io.Reader interface.
|
||||
func (rtb *readTrackingBody) Read(p []byte) (int, error) {
|
||||
if len(rtb.readBuf) > 0 {
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestRequestHandler(t *testing.T) {
|
||||
r.Header.Set("Pass-Header", "abc")
|
||||
|
||||
w := &fakeResponseWriter{}
|
||||
if !requestHandler(w, r) {
|
||||
if !requestHandlerWithInternalRoutes(w, r) {
|
||||
t.Fatalf("unexpected false is returned from requestHandler")
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ unauthorized_user:
|
||||
}
|
||||
responseExpected = `
|
||||
statusCode=401
|
||||
The provided authKey doesn't match -reloadAuthKey`
|
||||
Expected to receive non-empty authKey when -reloadAuthKey is set`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
if err := reloadAuthKey.Set(origAuthKey); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
@@ -545,8 +545,7 @@ func TestReadTrackingBody_RetrySuccess(t *testing.T) {
|
||||
f := func(s string, maxBodySize int) {
|
||||
t.Helper()
|
||||
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
|
||||
if !rtb.canRetry() {
|
||||
t.Fatalf("canRetry() must return true before reading anything")
|
||||
@@ -581,8 +580,7 @@ func TestReadTrackingBody_RetrySuccessPartialRead(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
// Check the case with partial read
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
buf := make([]byte, i)
|
||||
@@ -631,8 +629,7 @@ func TestReadTrackingBody_RetryFailureTooBigBody(t *testing.T) {
|
||||
f := func(s string, maxBodySize int) {
|
||||
t.Helper()
|
||||
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
|
||||
if !rtb.canRetry() {
|
||||
t.Fatalf("canRetry() must return true before reading anything")
|
||||
@@ -681,8 +678,7 @@ func TestReadTrackingBody_RetryFailureZeroOrNegativeMaxBodySize(t *testing.T) {
|
||||
f := func(s string, maxBodySize int) {
|
||||
t.Helper()
|
||||
|
||||
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
defer putReadTrackingBody(rtb)
|
||||
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
|
||||
|
||||
if !rtb.canRetry() {
|
||||
t.Fatalf("canRetry() must return true before reading anything")
|
||||
|
||||
@@ -596,7 +596,8 @@ var (
|
||||
&cli.Int64Flag{
|
||||
Name: vmRateLimit,
|
||||
Usage: "Optional data transfer rate limit in bytes per second.\n" +
|
||||
"By default, the rate limit is disabled. It can be useful for limiting load on source or destination databases.",
|
||||
"By default, the rate limit is disabled. It can be useful for limiting load on source or destination databases. \n" +
|
||||
"Rate limit is applied per worker, see `--vm-concurrency`.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: vmInterCluster,
|
||||
|
||||
@@ -40,15 +40,15 @@ type filter struct {
|
||||
labelValue string
|
||||
}
|
||||
|
||||
func (f filter) inRange(min, max int64) bool {
|
||||
func (f filter) inRange(minV, maxV int64) bool {
|
||||
fmin, fmax := f.min, f.max
|
||||
if min == 0 {
|
||||
fmin = min
|
||||
if minV == 0 {
|
||||
fmin = minV
|
||||
}
|
||||
if fmax == 0 {
|
||||
fmax = max
|
||||
fmax = maxV
|
||||
}
|
||||
return min <= fmax && fmin <= max
|
||||
return minV <= fmax && fmin <= maxV
|
||||
}
|
||||
|
||||
// NewClient creates and validates new Client
|
||||
@@ -59,13 +59,13 @@ func NewClient(cfg Config) (*Client, error) {
|
||||
return nil, fmt.Errorf("failed to open snapshot %q: %s", cfg.Snapshot, err)
|
||||
}
|
||||
c := &Client{DBReadOnly: db}
|
||||
min, max, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
|
||||
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse time in filter: %s", err)
|
||||
}
|
||||
c.filter = filter{
|
||||
min: min,
|
||||
max: max,
|
||||
min: minTime,
|
||||
max: maxTime,
|
||||
label: cfg.Filter.Label,
|
||||
labelValue: cfg.Filter.LabelValue,
|
||||
}
|
||||
|
||||
@@ -98,13 +98,13 @@ func aggrMin(values []float64) float64 {
|
||||
if pos < 0 {
|
||||
return nan
|
||||
}
|
||||
min := values[pos]
|
||||
minV := values[pos]
|
||||
for _, v := range values[pos+1:] {
|
||||
if !math.IsNaN(v) && v < min {
|
||||
min = v
|
||||
if !math.IsNaN(v) && v < minV {
|
||||
minV = v
|
||||
}
|
||||
}
|
||||
return min
|
||||
return minV
|
||||
}
|
||||
|
||||
func aggrMax(values []float64) float64 {
|
||||
@@ -112,13 +112,13 @@ func aggrMax(values []float64) float64 {
|
||||
if pos < 0 {
|
||||
return nan
|
||||
}
|
||||
max := values[pos]
|
||||
maxV := values[pos]
|
||||
for _, v := range values[pos+1:] {
|
||||
if !math.IsNaN(v) && v > max {
|
||||
max = v
|
||||
if !math.IsNaN(v) && v > maxV {
|
||||
maxV = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
return maxV
|
||||
}
|
||||
|
||||
func aggrDiff(values []float64) float64 {
|
||||
@@ -177,12 +177,12 @@ func aggrCount(values []float64) float64 {
|
||||
}
|
||||
|
||||
func aggrRange(values []float64) float64 {
|
||||
min := aggrMin(values)
|
||||
if math.IsNaN(min) {
|
||||
minV := aggrMin(values)
|
||||
if math.IsNaN(minV) {
|
||||
return nan
|
||||
}
|
||||
max := aggrMax(values)
|
||||
return max - min
|
||||
maxV := aggrMax(values)
|
||||
return maxV - minV
|
||||
}
|
||||
|
||||
func aggrMultiply(values []float64) float64 {
|
||||
|
||||
@@ -2594,17 +2594,17 @@ func transformMinMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
|
||||
}
|
||||
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
||||
values := s.Values
|
||||
min := aggrMin(values)
|
||||
if math.IsNaN(min) {
|
||||
min = 0
|
||||
minV := aggrMin(values)
|
||||
if math.IsNaN(minV) {
|
||||
minV = 0
|
||||
}
|
||||
max := aggrMax(values)
|
||||
if math.IsNaN(max) {
|
||||
max = 0
|
||||
maxV := aggrMax(values)
|
||||
if math.IsNaN(maxV) {
|
||||
maxV = 0
|
||||
}
|
||||
vRange := max - min
|
||||
vRange := maxV - minV
|
||||
for i, v := range values {
|
||||
v = (v - min) / vRange
|
||||
v = (v - minV) / vRange
|
||||
if math.IsInf(v, 0) {
|
||||
v = 0
|
||||
}
|
||||
@@ -2975,9 +2975,9 @@ func transformRemoveAbovePercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (ne
|
||||
}
|
||||
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
||||
values := s.Values
|
||||
max := aggrFunc(values)
|
||||
maxV := aggrFunc(values)
|
||||
for i, v := range values {
|
||||
if v > max {
|
||||
if v > maxV {
|
||||
values[i] = nan
|
||||
}
|
||||
}
|
||||
@@ -3035,9 +3035,9 @@ func transformRemoveBelowPercentile(ec *evalConfig, fe *graphiteql.FuncExpr) (ne
|
||||
}
|
||||
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
||||
values := s.Values
|
||||
min := aggrFunc(values)
|
||||
minV := aggrFunc(values)
|
||||
for i, v := range values {
|
||||
if v < min {
|
||||
if v < minV {
|
||||
values[i] = nan
|
||||
}
|
||||
}
|
||||
@@ -4514,11 +4514,11 @@ func transformOffsetToZero(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesF
|
||||
}
|
||||
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
||||
values := s.Values
|
||||
min := aggrMin(values)
|
||||
minV := aggrMin(values)
|
||||
for i, v := range values {
|
||||
values[i] = v - min
|
||||
values[i] = v - minV
|
||||
}
|
||||
s.Tags["offsetToZero"] = fmt.Sprintf("%g", min)
|
||||
s.Tags["offsetToZero"] = fmt.Sprintf("%g", minV)
|
||||
s.Name = fmt.Sprintf("offsetToZero(%s)", s.Name)
|
||||
s.expr = fe
|
||||
s.pathExpression = s.Name
|
||||
@@ -4567,29 +4567,29 @@ func transformPerSecond(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func nonNegativeDelta(curr, prev, max, min float64) (float64, float64) {
|
||||
if !math.IsNaN(max) && curr > max {
|
||||
func nonNegativeDelta(currV, prevV, maxV, minV float64) (float64, float64) {
|
||||
if !math.IsNaN(maxV) && currV > maxV {
|
||||
return nan, nan
|
||||
}
|
||||
if !math.IsNaN(min) && curr < min {
|
||||
if !math.IsNaN(minV) && currV < minV {
|
||||
return nan, nan
|
||||
}
|
||||
if math.IsNaN(curr) || math.IsNaN(prev) {
|
||||
return nan, curr
|
||||
if math.IsNaN(currV) || math.IsNaN(prevV) {
|
||||
return nan, currV
|
||||
}
|
||||
if curr >= prev {
|
||||
return curr - prev, curr
|
||||
if currV >= prevV {
|
||||
return currV - prevV, currV
|
||||
}
|
||||
if !math.IsNaN(max) {
|
||||
if math.IsNaN(min) {
|
||||
min = float64(0)
|
||||
if !math.IsNaN(maxV) {
|
||||
if math.IsNaN(minV) {
|
||||
minV = float64(0)
|
||||
}
|
||||
return max + 1 + curr - prev - min, curr
|
||||
return maxV + 1 + currV - prevV - minV, currV
|
||||
}
|
||||
if !math.IsNaN(min) {
|
||||
return curr - min, curr
|
||||
if !math.IsNaN(minV) {
|
||||
return currV - minV, currV
|
||||
}
|
||||
return nan, curr
|
||||
return nan, currV
|
||||
}
|
||||
|
||||
// See https://graphite.readthedocs.io/en/stable/functions.html#graphite.render.functions.threshold
|
||||
@@ -4941,8 +4941,8 @@ func transformSortByMinima(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesF
|
||||
}
|
||||
// Filter out series with all the values smaller than 0
|
||||
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
|
||||
max := aggrMax(s.Values)
|
||||
if math.IsNaN(max) || max <= 0 {
|
||||
maxV := aggrMax(s.Values)
|
||||
if math.IsNaN(maxV) || maxV <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s, nil
|
||||
|
||||
@@ -29,13 +29,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It overrides -httpAuth.*")
|
||||
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
maxConcurrentRequests = flag.Int("search.maxConcurrentRequests", getDefaultMaxConcurrentRequests(), "The maximum number of concurrent search requests. "+
|
||||
"It shouldn't be high, since a single request can saturate all the CPU cores, while many concurrently executed requests may require high amounts of memory. "+
|
||||
"See also -search.maxQueueDuration and -search.maxMemoryPerQuery")
|
||||
maxQueueDuration = flag.Duration("search.maxQueueDuration", 10*time.Second, "The maximum time the request waits for execution when -search.maxConcurrentRequests "+
|
||||
"limit is reached; see also -search.maxQueryDuration")
|
||||
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It overrides -httpAuth.*")
|
||||
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. It could be passed via authKey query arg. It overrides -httpAuth.*")
|
||||
logSlowQueryDuration = flag.Duration("search.logSlowQueryDuration", 5*time.Second, "Log queries with execution time exceeding this value. Zero disables slow query logging. "+
|
||||
"See also -search.logQueryMemoryUsage")
|
||||
vmalertProxyURL = flag.String("vmalert.proxyURL", "", "Optional URL for proxying requests to vmalert. For example, if -vmalert.proxyURL=http://vmalert:8880 , then alerting API requests such as /api/v1/rules from Grafana will be proxied to http://vmalert:8880/api/v1/rules")
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
@@ -1002,9 +1001,7 @@ func ExportBlocks(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline sear
|
||||
|
||||
sr := getStorageSearch()
|
||||
defer putStorageSearch(sr)
|
||||
startTime := time.Now()
|
||||
sr.Init(qt, vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
|
||||
indexSearchDuration.UpdateDuration(startTime)
|
||||
|
||||
// Start workers that call f in parallel on available CPU cores.
|
||||
workCh := make(chan *exportWork, gomaxprocs*8)
|
||||
@@ -1142,9 +1139,7 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
|
||||
defer vmstorage.WG.Done()
|
||||
|
||||
sr := getStorageSearch()
|
||||
startTime := time.Now()
|
||||
maxSeriesCount := sr.Init(qt, vmstorage.Storage, tfss, tr, sq.MaxMetrics, deadline.Deadline())
|
||||
indexSearchDuration.UpdateDuration(startTime)
|
||||
type blockRefs struct {
|
||||
brs []blockRef
|
||||
}
|
||||
@@ -1296,8 +1291,6 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
|
||||
return &rss, nil
|
||||
}
|
||||
|
||||
var indexSearchDuration = metrics.NewHistogram(`vm_index_search_duration_seconds`)
|
||||
|
||||
type blockRef struct {
|
||||
partRef storage.PartRef
|
||||
addr tmpBlockAddr
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/memory"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
|
||||
)
|
||||
@@ -142,10 +143,13 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
|
||||
WriteFederate(bb, rs)
|
||||
return sw.maybeFlushBuffer(bb)
|
||||
})
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
return fmt.Errorf("error during sending data to remote client: %w", err)
|
||||
}
|
||||
return sw.flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
var federateDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/federate"}`)
|
||||
@@ -226,10 +230,13 @@ func ExportCSVHandler(startTime time.Time, w http.ResponseWriter, r *http.Reques
|
||||
}()
|
||||
}
|
||||
err = <-doneCh
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
return fmt.Errorf("error during sending the exported csv data to remote client: %w", err)
|
||||
}
|
||||
return sw.flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
var exportCSVDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export/csv"}`)
|
||||
@@ -281,10 +288,13 @@ func ExportNativeHandler(startTime time.Time, w http.ResponseWriter, r *http.Req
|
||||
bb.B = dst
|
||||
return sw.maybeFlushBuffer(bb)
|
||||
})
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
return fmt.Errorf("error during sending native data to remote client: %w", err)
|
||||
}
|
||||
return sw.flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
var exportNativeDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export/native"}`)
|
||||
@@ -441,16 +451,19 @@ func exportHandler(qt *querytracer.Tracer, w http.ResponseWriter, cp *commonPara
|
||||
}()
|
||||
}
|
||||
err := <-doneCh
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
err = sw.flush()
|
||||
}
|
||||
if err == nil {
|
||||
if format == "promapi" {
|
||||
WriteExportPromAPIFooter(bw, qt)
|
||||
}
|
||||
err = bw.Flush()
|
||||
}
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
return fmt.Errorf("cannot send data to remote client: %w", err)
|
||||
}
|
||||
if err := sw.flush(); err != nil {
|
||||
return fmt.Errorf("cannot send data to remote client: %w", err)
|
||||
}
|
||||
if format == "promapi" {
|
||||
WriteExportPromAPIFooter(bw, qt)
|
||||
}
|
||||
return bw.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
type exportBlock struct {
|
||||
@@ -481,6 +494,8 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cp.deadline = searchutils.GetDeadlineForDelete(r, startTime)
|
||||
|
||||
if !cp.IsDefaultTimeRange() {
|
||||
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
|
||||
}
|
||||
|
||||
@@ -295,13 +295,13 @@ func aggrFuncMin(tss []*timeseries) []*timeseries {
|
||||
}
|
||||
dst := tss[0]
|
||||
for i := range dst.Values {
|
||||
min := dst.Values[i]
|
||||
minV := dst.Values[i]
|
||||
for _, ts := range tss {
|
||||
if math.IsNaN(min) || ts.Values[i] < min {
|
||||
min = ts.Values[i]
|
||||
if math.IsNaN(minV) || ts.Values[i] < minV {
|
||||
minV = ts.Values[i]
|
||||
}
|
||||
}
|
||||
dst.Values[i] = min
|
||||
dst.Values[i] = minV
|
||||
}
|
||||
return tss[:1]
|
||||
}
|
||||
@@ -313,13 +313,13 @@ func aggrFuncMax(tss []*timeseries) []*timeseries {
|
||||
}
|
||||
dst := tss[0]
|
||||
for i := range dst.Values {
|
||||
max := dst.Values[i]
|
||||
maxV := dst.Values[i]
|
||||
for _, ts := range tss {
|
||||
if math.IsNaN(max) || ts.Values[i] > max {
|
||||
max = ts.Values[i]
|
||||
if math.IsNaN(maxV) || ts.Values[i] > maxV {
|
||||
maxV = ts.Values[i]
|
||||
}
|
||||
}
|
||||
dst.Values[i] = max
|
||||
dst.Values[i] = maxV
|
||||
}
|
||||
return tss[:1]
|
||||
}
|
||||
@@ -793,7 +793,7 @@ func fillNaNsAtIdx(idx int, k float64, tss []*timeseries) {
|
||||
}
|
||||
}
|
||||
|
||||
func getIntK(k float64, max int) int {
|
||||
func getIntK(k float64, maxV int) int {
|
||||
if math.IsNaN(k) {
|
||||
return 0
|
||||
}
|
||||
@@ -801,38 +801,38 @@ func getIntK(k float64, max int) int {
|
||||
if kn < 0 {
|
||||
return 0
|
||||
}
|
||||
if kn > max {
|
||||
return max
|
||||
if kn > maxV {
|
||||
return maxV
|
||||
}
|
||||
return kn
|
||||
}
|
||||
|
||||
func minValue(values []float64) float64 {
|
||||
min := nan
|
||||
for len(values) > 0 && math.IsNaN(min) {
|
||||
min = values[0]
|
||||
minV := nan
|
||||
for len(values) > 0 && math.IsNaN(minV) {
|
||||
minV = values[0]
|
||||
values = values[1:]
|
||||
}
|
||||
for _, v := range values {
|
||||
if !math.IsNaN(v) && v < min {
|
||||
min = v
|
||||
if !math.IsNaN(v) && v < minV {
|
||||
minV = v
|
||||
}
|
||||
}
|
||||
return min
|
||||
return minV
|
||||
}
|
||||
|
||||
func maxValue(values []float64) float64 {
|
||||
max := nan
|
||||
for len(values) > 0 && math.IsNaN(max) {
|
||||
max = values[0]
|
||||
maxV := nan
|
||||
for len(values) > 0 && math.IsNaN(maxV) {
|
||||
maxV = values[0]
|
||||
values = values[1:]
|
||||
}
|
||||
for _, v := range values {
|
||||
if !math.IsNaN(v) && v > max {
|
||||
max = v
|
||||
if !math.IsNaN(v) && v > maxV {
|
||||
maxV = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
return maxV
|
||||
}
|
||||
|
||||
func avgValue(values []float64) float64 {
|
||||
|
||||
@@ -483,8 +483,11 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
|
||||
mLeft, mRight := createTimeseriesMapByTagSet(bfa.be, bfa.left, bfa.right)
|
||||
var rvs []*timeseries
|
||||
|
||||
for _, tss := range mLeft {
|
||||
rvs = append(rvs, tss...)
|
||||
for k, tss := range mLeft {
|
||||
tssLeft := removeEmptySeries(tss)
|
||||
// re-assign modified slice to map, since it can be referred later
|
||||
mLeft[k] = tssLeft
|
||||
rvs = append(rvs, tssLeft...)
|
||||
}
|
||||
// Sort left-hand-side series by metric name as Prometheus does.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5393
|
||||
@@ -497,7 +500,10 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
|
||||
rvs = append(rvs, tssRight...)
|
||||
continue
|
||||
}
|
||||
fillLeftNaNsWithRightValues(tssLeft, tssRight)
|
||||
fillLeftNaNsWithRightValuesOrMerge(tssLeft, tssRight)
|
||||
// tssRight might be filled with NaNs after merge
|
||||
tssRight = removeEmptySeries(tssRight)
|
||||
rvs = append(rvs, tssRight...)
|
||||
}
|
||||
// Sort the added right-hand-side series by metric name as Prometheus does.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5393
|
||||
@@ -526,6 +532,35 @@ func fillLeftNaNsWithRightValues(tssLeft, tssRight []*timeseries) {
|
||||
}
|
||||
}
|
||||
|
||||
// fill gaps in tssLeft with values from tssRight when labels match
|
||||
// Set NaNs to tssRight when tssLeft has corresponding values
|
||||
// or if tssLeft and tssRight can be merged.
|
||||
//
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7759
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7640
|
||||
func fillLeftNaNsWithRightValuesOrMerge(tssLeft, tssRight []*timeseries) {
|
||||
for _, tsLeft := range tssLeft {
|
||||
valuesLeft := tsLeft.Values
|
||||
nameLeft := tsLeft.MetricName.String()
|
||||
for i, v := range valuesLeft {
|
||||
leftIsNaN := math.IsNaN(v)
|
||||
for _, tsRight := range tssRight {
|
||||
canBeMerged := nameLeft == tsRight.MetricName.String()
|
||||
valueRight := tsRight.Values[i]
|
||||
if leftIsNaN && canBeMerged {
|
||||
// fill NaNs with valueRight if labels match
|
||||
valuesLeft[i] = valueRight
|
||||
}
|
||||
if !leftIsNaN || canBeMerged {
|
||||
// set NaN to valueRight if valueLeft is not NaN
|
||||
// or if left and right can be merged
|
||||
tsRight.Values[i] = nan
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func binaryOpIfnot(bfa *binaryOpFuncArg) ([]*timeseries, error) {
|
||||
mLeft, mRight := createTimeseriesMapByTagSet(bfa.be, bfa.left, bfa.right)
|
||||
var rvs []*timeseries
|
||||
|
||||
@@ -4461,9 +4461,9 @@ func TestExecSuccess(t *testing.T) {
|
||||
t.Run(`histogram_quantile(nan-bucket-count-some)`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `round(histogram_quantile(0.6,
|
||||
label_set(90, "foo", "bar", "le", "10")
|
||||
or label_set(NaN, "foo", "bar", "le", "30")
|
||||
or label_set(300, "foo", "bar", "le", "+Inf")
|
||||
union(label_set(90, "foo", "bar", "le", "10"),
|
||||
label_set(NaN, "foo", "bar", "le", "30"),
|
||||
label_set(300, "foo", "bar", "le", "+Inf"))
|
||||
),0.01)`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
@@ -9409,7 +9409,384 @@ func TestExecSuccess(t *testing.T) {
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`nan or on() series`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left side returns NaNs only, so the right side should replace its values and labels
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7759
|
||||
q := `(label_set(1, "a", "a", "b", "b1") == 0) or on(a) label_set(2, "a", "a", "b", "b2")`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{2, 2, 2, 2, 2, 2},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b2"),
|
||||
}}
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series with NaNs or scalar`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `(label_set(time() >= 1600, "a", "a", "b", "b1")) or 1`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1, 1, 1, 1, 1, 1},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() scalar`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7640
|
||||
q := `(label_set(time() > 1200, "a", "a", "b", "b1")) or on() vector(0)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{0, 0, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() series`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left side + right side
|
||||
q := `(label_set(time() <= 1200, "a", "a", "b", "b1")) or on(a) label_set(time() > 1200, "a", "a", "b", "b2")`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b2"),
|
||||
}}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series with no NaNs or on() series`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left side contains all needed values, so the right side should be dropped
|
||||
q := `(label_set(time() < 3000, "a", "a", "b", "b1")) or on(a) label_set(time() > 3000, "a", "a", "b", "b2")`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() series with overlap`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left overlap with right
|
||||
q := `(label_set(time() <= 1500, "a", "a", "b", "b1")) or on(a) label_set(time() > 1100, "a", "a", "b", "b2")`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, 1400, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b2"),
|
||||
}}
|
||||
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or on() series merge`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// left + right for same series
|
||||
q := `(label_set(time() <= 1200, "a", "a", "b", "b1")) or on(a) label_set(time() > 1400, "a", "a", "b", "b1")`
|
||||
r := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("a"),
|
||||
Value: []byte("a"),
|
||||
}, {
|
||||
Key: []byte("b"),
|
||||
Value: []byte("b1"),
|
||||
}}
|
||||
|
||||
resultExpected := []netstorage.Result{r}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`scalar or timeseries`, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := `time() > 1400 or label_set(123, "foo", "bar")`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{123, 123, 123, 123, 123, 123},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{{
|
||||
Key: []byte("foo"),
|
||||
Value: []byte("bar"),
|
||||
}}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`series or many series`, func(t *testing.T) {
|
||||
//load 1m
|
||||
// foo{a="a", b="1"} 1 0 1 1 1
|
||||
// bar{a="a", b="2"} 2 2 2 2 2
|
||||
// bar{a="a", b="3"} 3 3 3 3 3
|
||||
//
|
||||
//eval range from 0 to 4m step 1m foo!=0 or on (a) bar
|
||||
// foo{a="a", b="1"} 1 _ 1 1 1
|
||||
// bar{a="a", b="2"} _ 2 _ _ _
|
||||
// bar{a="a", b="3"} _ 3 _ _ _
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1200, "x", "foo"),
|
||||
) or on(x) (
|
||||
label_set(time()+1, "x", "foo", "y", "bar"),
|
||||
label_set(time()+2, "y", "baz", "x", "foo"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, 1201, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
{Key: []byte("y"), Value: []byte("bar")},
|
||||
}
|
||||
r3 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, 1202, nan, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r3.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
{Key: []byte("y"), Value: []byte("baz")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2, r3}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`many series or series`, func(t *testing.T) {
|
||||
//load 1m
|
||||
// foo{a="a", b="1"} 1 0 1 1 1
|
||||
// foo{a="a", b="2"} 2 2 2 2 2
|
||||
// bar{a="a", b="3"} 3 3 3 3 3
|
||||
//
|
||||
//eval range from 0 to 4m step 1m foo!=0 or on (a) bar
|
||||
// foo{a="a", b="1"} 1 _ 1 1 1
|
||||
// foo{a="a", b="2"} 2 2 2 2 2
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1200, "x", "foo"),
|
||||
label_set(time()+1, "x", "foo", "y","baz"),
|
||||
) or on(x) (
|
||||
label_set(time()+2, "x", "foo", "y", "bar"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1001, 1201, 1401, 1601, 1801, 2001},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("x"), Value: []byte("foo")},
|
||||
{Key: []byte("y"), Value: []byte("baz")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`many series or series with no merge`, func(t *testing.T) {
|
||||
// load 1m
|
||||
// foo{job="a1", a="a"} 0 0 1 1 0
|
||||
// foo{job="a2", a="a"} 1 1 0 0 0
|
||||
// foo{job="a3", a="a"} 1 1 1 1 1
|
||||
// foo{job="a4", a="a"} 1 1 1 1 1
|
||||
//
|
||||
//eval range from 0 to 4m step 1m (foo{job=~"a1|a2"} == 0) or on (a) (foo{job=~"a3|a4"} == 1)
|
||||
// foo{job="a1", a="a"} 0 0 _ _ 0
|
||||
// foo{job="a2", a="a"} _ _ 0 0 0
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1400, "job", "a1", "a", "a"),
|
||||
label_set(time()>=1400, "job", "a2", "a", "a"),
|
||||
) or on(a) (
|
||||
label_set(time(), "job", "a3", "a", "a"),
|
||||
label_set(time(), "job", "a4", "a", "a"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a1")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a2")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
t.Run(`many series or series with merge`, func(t *testing.T) {
|
||||
// load 1m
|
||||
// foo{job="a1", a="a"} 0 0 1 1 0
|
||||
// foo{job="a2", a="a"} 1 1 1 0 0
|
||||
// foo{job="a3", a="a"} 1 1 1 1 1
|
||||
// foo{job="a4", a="a"} 1 1 1 1 1
|
||||
//
|
||||
//eval range from 0 to 4m step 1m (foo{job=~"a1|a2"} == 0) or on (a) (foo{job=~"a3|a4"} == 1)
|
||||
// foo{job="a1", a="a"} 0 0 _ _ 0
|
||||
// foo{job="a2", a="a"} _ _ _ 0 0
|
||||
// foo{job="a3", a="a"} _ _ 1 _ _
|
||||
// foo{job="a4", a="a"} _ _ 1 _ _
|
||||
// https://github.com/prometheus/prometheus/tree/main/promql/promqltest
|
||||
t.Parallel()
|
||||
q := `(
|
||||
label_set(time()!=1400, "job", "a1", "a", "a"),
|
||||
label_set(time()>=1600, "job", "a2", "a", "a"),
|
||||
) or on(a) (
|
||||
label_set(time(), "job", "a3", "a", "a"),
|
||||
label_set(time(), "job", "a4", "a", "a"),
|
||||
)`
|
||||
r1 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{1000, 1200, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r1.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a1")},
|
||||
}
|
||||
r2 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r2.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a2")},
|
||||
}
|
||||
r3 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r3.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a3")},
|
||||
}
|
||||
r4 := netstorage.Result{
|
||||
MetricName: metricNameExpected,
|
||||
Values: []float64{nan, nan, 1400, nan, nan, nan},
|
||||
Timestamps: timestampsExpected,
|
||||
}
|
||||
r4.MetricName.Tags = []storage.Tag{
|
||||
{Key: []byte("a"), Value: []byte("a")},
|
||||
{Key: []byte("job"), Value: []byte("a4")},
|
||||
}
|
||||
resultExpected := []netstorage.Result{r1, r2, r3, r4}
|
||||
f(q, resultExpected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecError(t *testing.T) {
|
||||
|
||||
@@ -374,8 +374,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
|
||||
preFunc := func(_ []float64, _ []int64) {}
|
||||
funcName = strings.ToLower(funcName)
|
||||
if rollupFuncsRemoveCounterResets[funcName] {
|
||||
preFunc = func(values []float64, _ []int64) {
|
||||
removeCounterResets(values)
|
||||
preFunc = func(values []float64, timestamps []int64) {
|
||||
removeCounterResets(values, timestamps, lookbackDelta)
|
||||
}
|
||||
}
|
||||
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
|
||||
@@ -486,8 +486,8 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
|
||||
for _, aggrFuncName := range aggrFuncNames {
|
||||
if rollupFuncsRemoveCounterResets[aggrFuncName] {
|
||||
// There is no need to save the previous preFunc, since it is either empty or the same.
|
||||
preFunc = func(values []float64, _ []int64) {
|
||||
removeCounterResets(values)
|
||||
preFunc = func(values []float64, timestamps []int64) {
|
||||
removeCounterResets(values, timestamps, lookbackDelta)
|
||||
}
|
||||
}
|
||||
rf := rollupAggrFuncs[aggrFuncName]
|
||||
@@ -520,7 +520,8 @@ type rollupFuncArg struct {
|
||||
// Timestamps for values.
|
||||
timestamps []int64
|
||||
|
||||
// Real value preceding values without restrictions on staleness interval.
|
||||
// Real value preceding values.
|
||||
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
|
||||
realPrevValue float64
|
||||
|
||||
// Real value which goes after values.
|
||||
@@ -764,10 +765,18 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
|
||||
}
|
||||
rfa.values = values[i:j]
|
||||
rfa.timestamps = timestamps[i:j]
|
||||
rfa.realPrevValue = nan
|
||||
if i > 0 {
|
||||
rfa.realPrevValue = values[i-1]
|
||||
} else {
|
||||
rfa.realPrevValue = nan
|
||||
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
|
||||
// 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 {
|
||||
rfa.realPrevValue = prevValue
|
||||
}
|
||||
}
|
||||
if j < len(values) {
|
||||
rfa.realNextValue = values[j]
|
||||
@@ -891,7 +900,7 @@ func getMaxPrevInterval(scrapeInterval int64) int64 {
|
||||
return scrapeInterval + scrapeInterval/8
|
||||
}
|
||||
|
||||
func removeCounterResets(values []float64) {
|
||||
func removeCounterResets(values []float64, timestamps []int64, maxStalenessInterval int64) {
|
||||
// There is no need in handling NaNs here, since they are impossible
|
||||
// on values from vmstorage.
|
||||
if len(values) == 0 {
|
||||
@@ -910,6 +919,16 @@ func removeCounterResets(values []float64) {
|
||||
correction += prevValue
|
||||
}
|
||||
}
|
||||
if i > 0 && maxStalenessInterval > 0 {
|
||||
gap := timestamps[i] - timestamps[i-1]
|
||||
if gap > maxStalenessInterval {
|
||||
// reset correction if gap between samples exceeds staleness interval
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
|
||||
correction = 0
|
||||
prevValue = v
|
||||
continue
|
||||
}
|
||||
}
|
||||
prevValue = v
|
||||
values[i] = v + correction
|
||||
// Check again, there could be precision error in float operations,
|
||||
@@ -1682,9 +1701,9 @@ func rollupRateOverSum(rfa *rollupFuncArg) float64 {
|
||||
}
|
||||
|
||||
func rollupRange(rfa *rollupFuncArg) float64 {
|
||||
max := rollupMax(rfa)
|
||||
min := rollupMin(rfa)
|
||||
return max - min
|
||||
maxV := rollupMax(rfa)
|
||||
minV := rollupMin(rfa)
|
||||
return maxV - minV
|
||||
}
|
||||
|
||||
func rollupSum2(rfa *rollupFuncArg) float64 {
|
||||
@@ -2192,38 +2211,38 @@ func rollupClose(rfa *rollupFuncArg) float64 {
|
||||
|
||||
func rollupHigh(rfa *rollupFuncArg) float64 {
|
||||
values := getCandlestickValues(rfa)
|
||||
max := getFirstValueForCandlestick(rfa)
|
||||
if math.IsNaN(max) {
|
||||
maxV := getFirstValueForCandlestick(rfa)
|
||||
if math.IsNaN(maxV) {
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
}
|
||||
max = values[0]
|
||||
maxV = values[0]
|
||||
values = values[1:]
|
||||
}
|
||||
for _, v := range values {
|
||||
if v > max {
|
||||
max = v
|
||||
if v > maxV {
|
||||
maxV = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
return maxV
|
||||
}
|
||||
|
||||
func rollupLow(rfa *rollupFuncArg) float64 {
|
||||
values := getCandlestickValues(rfa)
|
||||
min := getFirstValueForCandlestick(rfa)
|
||||
if math.IsNaN(min) {
|
||||
minV := getFirstValueForCandlestick(rfa)
|
||||
if math.IsNaN(minV) {
|
||||
if len(values) == 0 {
|
||||
return nan
|
||||
}
|
||||
min = values[0]
|
||||
minV = values[0]
|
||||
values = values[1:]
|
||||
}
|
||||
for _, v := range values {
|
||||
if v < min {
|
||||
min = v
|
||||
if v < minV {
|
||||
minV = v
|
||||
}
|
||||
}
|
||||
return min
|
||||
return minV
|
||||
}
|
||||
|
||||
func rollupModeOverTime(rfa *rollupFuncArg) float64 {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/VictoriaMetrics/metricsql"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -115,31 +117,49 @@ func TestRollupIderivDuplicateTimestamps(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveCounterResets(t *testing.T) {
|
||||
removeCounterResets(nil)
|
||||
removeCounterResets(nil, nil, 0)
|
||||
|
||||
values := append([]float64{}, testValues...)
|
||||
removeCounterResets(values)
|
||||
timestamps := append([]int64{}, testTimestamps...)
|
||||
removeCounterResets(values, timestamps, 0)
|
||||
valuesExpected := []float64{123, 157, 167, 188, 221, 255, 320, 332, 364, 396, 398, 398}
|
||||
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
|
||||
|
||||
// removeCounterResets doesn't expect negative values, so it doesn't work properly with them.
|
||||
values = []float64{-100, -200, -300, -400}
|
||||
removeCounterResets(values)
|
||||
valuesExpected = []float64{-100, -100, -100, -100}
|
||||
timestampsExpected := []int64{0, 1, 2, 3}
|
||||
removeCounterResets(values, timestampsExpected, 0)
|
||||
valuesExpected = []float64{-100, -100, -100, -100}
|
||||
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
|
||||
|
||||
// verify how partial counter reset is handled.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2787
|
||||
values = []float64{100, 95, 120, 119, 139, 50}
|
||||
removeCounterResets(values)
|
||||
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
|
||||
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
|
||||
removeCounterResets(values, timestampsExpected, 0)
|
||||
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
|
||||
testRowsEqual(t, values, timestampsExpected, valuesExpected, timestampsExpected)
|
||||
|
||||
// verify that staleness interval is respected during resets
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8072
|
||||
values = []float64{10, 12, 14, 4, 6, 8, 6, 8, 4, 6}
|
||||
timestamps = []int64{10, 20, 30, 60, 70, 80, 90, 100, 120, 130}
|
||||
valuesExpected = []float64{10, 12, 14, 4, 6, 8, 14, 16, 4, 6}
|
||||
removeCounterResets(values, timestamps, 10)
|
||||
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
|
||||
|
||||
// verify that staleness is respected if there was no counter reset
|
||||
// but correction was made previously
|
||||
values = []float64{10, 12, 2, 4}
|
||||
timestamps = []int64{10, 20, 30, 60}
|
||||
valuesExpected = []float64{10, 12, 14, 4}
|
||||
removeCounterResets(values, timestamps, 10)
|
||||
testRowsEqual(t, values, timestamps, valuesExpected, timestamps)
|
||||
|
||||
// verify results always increase monotonically with possible float operations precision error
|
||||
values = []float64{34.094223, 2.7518, 2.140669, 0.044878, 1.887095, 2.546569, 2.490149, 0.045, 0.035684, 0.062454, 0.058296}
|
||||
removeCounterResets(values)
|
||||
timestampsExpected = []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
removeCounterResets(values, timestampsExpected, 0)
|
||||
var prev float64
|
||||
for i, v := range values {
|
||||
if v < prev {
|
||||
@@ -164,7 +184,7 @@ func TestDeltaValues(t *testing.T) {
|
||||
|
||||
// remove counter resets
|
||||
values = append([]float64{}, testValues...)
|
||||
removeCounterResets(values)
|
||||
removeCounterResets(values, testTimestamps, 0)
|
||||
deltaValues(values)
|
||||
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, 0}
|
||||
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
|
||||
@@ -186,7 +206,7 @@ func TestDerivValues(t *testing.T) {
|
||||
|
||||
// remove counter resets
|
||||
values = append([]float64{}, testValues...)
|
||||
removeCounterResets(values)
|
||||
removeCounterResets(values, testTimestamps, 0)
|
||||
derivValues(values, testTimestamps)
|
||||
valuesExpected = []float64{3400, 1111.111111111111, 1750, 2538.4615384615386, 3090.909090909091, 3611.1111111111113,
|
||||
6000, 1882.3529411764705, 1777.7777777777778, 400, 0, 0}
|
||||
@@ -217,7 +237,7 @@ func testRollupFunc(t *testing.T, funcName string, args []any, vExpected float64
|
||||
rfa.timestamps = append(rfa.timestamps, testTimestamps...)
|
||||
rfa.window = rfa.timestamps[len(rfa.timestamps)-1] - rfa.timestamps[0]
|
||||
if rollupFuncsRemoveCounterResets[funcName] {
|
||||
removeCounterResets(rfa.values)
|
||||
removeCounterResets(rfa.values, rfa.timestamps, 0)
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
v := rf(&rfa)
|
||||
@@ -1587,3 +1607,229 @@ func TestRollupDelta(t *testing.T) {
|
||||
f(1, nan, nan, nil, 0)
|
||||
f(100, nan, nan, nil, 0)
|
||||
}
|
||||
|
||||
func TestRollupDeltaWithStaleness(t *testing.T) {
|
||||
// there is a gap between samples in the dataset below
|
||||
timestamps := []int64{0, 15000, 30000, 70000}
|
||||
values := []float64{1, 1, 1, 1}
|
||||
|
||||
// if step > gap, then delta will always respect value before gap
|
||||
t.Run("step>gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
// even if LookbackDelta < gap
|
||||
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
LookbackDelta: 10e3,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
|
||||
// as LookbackDelta=0 ignores staleness
|
||||
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
LookbackDelta: 0,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta>0 then delta will respect value before gap
|
||||
// only if it is not stale according to LookbackDelta
|
||||
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
LookbackDelta: 30e3,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 1}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// there is a staleness marker between samples in the dataset below
|
||||
timestamps = []int64{0, 10000, 20000, 30000, 40000}
|
||||
values = []float64{1, 1, 1, decimal.StaleNaN, 1}
|
||||
|
||||
t.Run("staleness marker", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupDelta,
|
||||
Start: 0,
|
||||
End: 40000,
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 10 {
|
||||
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, nan, 1}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRollupIncreasePureWithStaleness(t *testing.T) {
|
||||
// there is a gap between samples in the dataset below
|
||||
timestamps := []int64{0, 15000, 30000, 70000}
|
||||
values := []float64{1, 1, 1, 1}
|
||||
|
||||
// if step > gap, then delta will always respect value before gap
|
||||
t.Run("step>gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
// even if LookbackDelta < gap
|
||||
t.Run("step>gap;LookbackDelta<gap", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 45000,
|
||||
LookbackDelta: 10e3,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 7 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0}
|
||||
timestampsExpected := []int64{0, 45e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta==0 then delta will always respect value before gap
|
||||
// as LookbackDelta=0 ignores staleness
|
||||
t.Run("step<gap;LookbackDelta=0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
LookbackDelta: 0,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 0}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// if step < gap and LookbackDelta>0 then delta will respect value before gap
|
||||
// only if it is not stale according to LookbackDelta
|
||||
t.Run("step<gap;LookbackDelta>0", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 70000,
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
LookbackDelta: 30e3,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 8 {
|
||||
t.Fatalf("expecting 8 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, 0, 0, 0, 0, 1}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3, 50e3, 60e3, 70e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
|
||||
// there is a staleness marker between samples in the dataset below
|
||||
timestamps = []int64{0, 10000, 20000, 30000, 40000}
|
||||
values = []float64{1, 1, 1, decimal.StaleNaN, 1}
|
||||
|
||||
t.Run("staleness marker", func(t *testing.T) {
|
||||
rc := rollupConfig{
|
||||
Func: rollupIncreasePure,
|
||||
Start: 0,
|
||||
End: 40000,
|
||||
Step: 10000,
|
||||
Window: 0,
|
||||
MaxPointsPerSeries: 1e4,
|
||||
}
|
||||
rc.Timestamps = rc.getTimestamps()
|
||||
gotValues, samplesScanned := rc.Do(nil, values, timestamps)
|
||||
if samplesScanned != 10 {
|
||||
t.Fatalf("expecting 10 samplesScanned from rollupConfig.Do; got %d", samplesScanned)
|
||||
}
|
||||
valuesExpected := []float64{1, 0, 0, nan, 1}
|
||||
timestampsExpected := []int64{0, 10e3, 20e3, 30e3, 40e3}
|
||||
testRowsEqual(t, gotValues, rc.Timestamps, valuesExpected, timestampsExpected)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
var (
|
||||
maxExportDuration = flag.Duration("search.maxExportDuration", time.Hour*24*30, "The maximum duration for /api/v1/export call")
|
||||
maxDeleteDuration = flag.Duration("search.maxDeleteDuration", time.Minute*5, "The maximum duration for /api/v1/admin/tsdb/delete_series call")
|
||||
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")
|
||||
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
|
||||
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
|
||||
@@ -58,6 +59,12 @@ func GetDeadlineForLabelsAPI(r *http.Request, startTime time.Time) Deadline {
|
||||
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxLabelsAPIDuration")
|
||||
}
|
||||
|
||||
// GetDeadlineForDelete returns deadline for the given request to /api/v1/admin/tsdb/delete_series.
|
||||
func GetDeadlineForDelete(r *http.Request, startTime time.Time) Deadline {
|
||||
dMax := maxDeleteDuration.Milliseconds()
|
||||
return getDeadlineWithMaxDuration(r, startTime, dMax, "-search.maxDeleteDuration")
|
||||
}
|
||||
|
||||
func getDeadlineWithMaxDuration(r *http.Request, startTime time.Time, dMax int64, flagHint string) Deadline {
|
||||
d, err := httputils.GetDuration(r, "timeout", 0)
|
||||
if err != nil {
|
||||
|
||||
@@ -99,7 +99,8 @@ func TestParseMetricSelectorSuccess(t *testing.T) {
|
||||
f(`{foo="bar"}`)
|
||||
f(`{:f:oo=~"bar.+"}`)
|
||||
f(`foo {bar != "baz"}`)
|
||||
f(` foo { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
|
||||
f(` { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
|
||||
f(` { bar !~ "^ddd(x+)$", a="ss", "foo"} `)
|
||||
f(`(foo)`)
|
||||
f(`\п\р\и\в\е\т{\ы="111"}`)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.876c56b7.css",
|
||||
"main.js": "./static/js/main.caf36c39.js",
|
||||
"main.css": "./static/css/main.7fa18e1b.css",
|
||||
"main.js": "./static/js/main.ba08300f.js",
|
||||
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
|
||||
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.876c56b7.css",
|
||||
"static/js/main.caf36c39.js"
|
||||
"static/css/main.7fa18e1b.css",
|
||||
"static/js/main.ba08300f.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.caf36c39.js"></script><link href="./static/css/main.876c56b7.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./favicon.svg"/><link rel="mask-icon" href="./favicon.svg" color="#000000"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5"/><meta name="theme-color" content="#000000"/><meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/><link rel="manifest" href="./manifest.json"/><title>vmui</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:site" content="@https://victoriametrics.com/"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:image" content="./preview.jpg"><meta property="og:type" content="website"><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 defer="defer" src="./static/js/main.ba08300f.js"></script><link href="./static/css/main.7fa18e1b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
1
app/vmselect/vmui/static/css/main.7fa18e1b.css
Normal file
1
app/vmselect/vmui/static/css/main.7fa18e1b.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
2
app/vmselect/vmui/static/js/main.ba08300f.js
Normal file
2
app/vmselect/vmui/static/js/main.ba08300f.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,6 +67,8 @@ var (
|
||||
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
|
||||
cacheSizeIndexDBDataBlocks = flagutil.NewBytes("storage.cacheSizeIndexDBDataBlocks", 0, "Overrides max size for indexdb/dataBlocks cache. "+
|
||||
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
|
||||
cacheSizeIndexDBDataBlocksSparse = flagutil.NewBytes("storage.cacheSizeIndexDBDataBlocksSparse", 0, "Overrides max size for indexdb/dataBlocksSparse cache. "+
|
||||
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
|
||||
cacheSizeIndexDBTagFilters = flagutil.NewBytes("storage.cacheSizeIndexDBTagFilters", 0, "Overrides max size for indexdb/tagFiltersToMetricIDs cache. "+
|
||||
"See https://docs.victoriametrics.com/single-server-victoriametrics/#cache-tuning")
|
||||
)
|
||||
@@ -100,6 +102,7 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
|
||||
storage.SetTagFiltersCacheSize(cacheSizeIndexDBTagFilters.IntN())
|
||||
mergeset.SetIndexBlocksCacheSize(cacheSizeIndexDBIndexBlocks.IntN())
|
||||
mergeset.SetDataBlocksCacheSize(cacheSizeIndexDBDataBlocks.IntN())
|
||||
mergeset.SetDataBlocksSparseCacheSize(cacheSizeIndexDBDataBlocksSparse.IntN())
|
||||
|
||||
if retentionPeriod.Duration() < 24*time.Hour {
|
||||
logger.Fatalf("-retentionPeriod cannot be smaller than a day; got %s", retentionPeriod)
|
||||
@@ -581,6 +584,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="storage/next_day_metric_ids"}`, m.NextDayMetricIDCacheSize)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="storage/indexBlocks"}`, tm.IndexBlocksCacheSize)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheSize)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheSize)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheSize)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheSize)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_entries{type="storage/regexps"}`, uint64(storage.RegexpCacheSize()))
|
||||
@@ -592,6 +596,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/metricName"}`, m.MetricNameCacheSizeBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/indexBlocks"}`, tm.IndexBlocksCacheSizeBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheSizeBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheSizeBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheSizeBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/date_metricID"}`, m.DateMetricIDCacheSizeBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_bytes{type="storage/hour_metric_ids"}`, m.HourMetricIDCacheSizeBytes)
|
||||
@@ -606,6 +611,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="storage/metricName"}`, m.MetricNameCacheSizeMaxBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="storage/indexBlocks"}`, tm.IndexBlocksCacheSizeMaxBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheSizeMaxBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheSizeMaxBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheSizeMaxBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheSizeMaxBytes)
|
||||
metrics.WriteGaugeUint64(w, `vm_cache_size_max_bytes{type="storage/regexps"}`, uint64(storage.RegexpCacheMaxSizeBytes()))
|
||||
@@ -616,6 +622,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="storage/metricName"}`, m.MetricNameCacheRequests)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="storage/indexBlocks"}`, tm.IndexBlocksCacheRequests)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheRequests)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheRequests)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheRequests)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheRequests)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_requests_total{type="storage/regexps"}`, storage.RegexpCacheRequests())
|
||||
@@ -626,6 +633,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="storage/metricName"}`, m.MetricNameCacheMisses)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="storage/indexBlocks"}`, tm.IndexBlocksCacheMisses)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/dataBlocks"}`, idbm.DataBlocksCacheMisses)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/dataBlocksSparse"}`, idbm.DataBlocksSparseCacheMisses)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/indexBlocks"}`, idbm.IndexBlocksCacheMisses)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="indexdb/tagFiltersToMetricIDs"}`, idbm.TagFiltersToMetricIDsCacheMisses)
|
||||
metrics.WriteCounterUint64(w, `vm_cache_misses_total{type="storage/regexps"}`, storage.RegexpCacheMisses())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23.4 AS build-web-stage
|
||||
FROM golang:1.23.6 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
@@ -6,7 +6,7 @@ COPY web/ /build/
|
||||
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
|
||||
|
||||
FROM alpine:3.21.0
|
||||
FROM alpine:3.21.2
|
||||
USER root
|
||||
|
||||
COPY --from=build-web-stage /build/web-amd64 /app/web
|
||||
|
||||
@@ -1 +1 @@
|
||||
FAST_REFRESH=false
|
||||
VITE_APP_TYPE=victoriametrics
|
||||
|
||||
1
app/vmui/packages/vmui/.env.victorialogs
Normal file
1
app/vmui/packages/vmui/.env.victorialogs
Normal file
@@ -0,0 +1 @@
|
||||
VITE_APP_TYPE=victorialogs
|
||||
1
app/vmui/packages/vmui/.env.vmanomaly
Normal file
1
app/vmui/packages/vmui/.env.vmanomaly
Normal file
@@ -0,0 +1 @@
|
||||
VITE_APP_TYPE=vmanomaly
|
||||
@@ -1,48 +0,0 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": { "jsx": true },
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "varsIgnorePattern": "^_" }],
|
||||
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
||||
"react/jsx-max-props-per-line":[1, { "maximum": 1 }],
|
||||
"react/jsx-first-prop-new-line": [1, "multiline"],
|
||||
"object-curly-spacing": [2, "always"],
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "double"],
|
||||
"semi": ["error", "always"],
|
||||
"react/prop-types": 0
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React", // Pragma to use, default to "React"
|
||||
"version": "detect"
|
||||
},
|
||||
"linkComponents": [
|
||||
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
|
||||
"Hyperlink",
|
||||
{
|
||||
"name": "Link", "linkAttribute": "to"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/* eslint-disable */
|
||||
const { override, addExternalBabelPlugin, addWebpackAlias, addWebpackPlugin } = require("customize-cra");
|
||||
const webpack = require("webpack");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// This will replace the default check
|
||||
const pathIndexHTML = (() => {
|
||||
switch (process.env.REACT_APP_TYPE) {
|
||||
case 'logs':
|
||||
return 'src/html/victorialogs.html';
|
||||
case 'anomaly':
|
||||
return 'src/html/vmanomaly.html';
|
||||
default:
|
||||
return 'src/html/victoriametrics.html';
|
||||
}
|
||||
})();
|
||||
const fileContent = fs.readFileSync(path.resolve(__dirname, pathIndexHTML), 'utf8');
|
||||
fs.writeFileSync(path.resolve(__dirname, 'public/index.html'), fileContent);
|
||||
|
||||
module.exports = override(
|
||||
addExternalBabelPlugin("@babel/plugin-proposal-nullish-coalescing-operator"),
|
||||
addWebpackAlias({
|
||||
"react": "preact/compat",
|
||||
"react-dom/test-utils": "preact/test-utils",
|
||||
"react-dom": "preact/compat", // Must be below test-utils
|
||||
"react/jsx-runtime": "preact/jsx-runtime"
|
||||
}),
|
||||
addWebpackPlugin(
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/\.\/App/,
|
||||
function (resource) {
|
||||
if (process.env.REACT_APP_TYPE === "logs") {
|
||||
resource.request = "./AppLogs";
|
||||
}
|
||||
if (process.env.REACT_APP_TYPE === "anomaly") {
|
||||
resource.request = "./AppAnomaly";
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
23
app/vmui/packages/vmui/config/plugins/dynamicIndexHtml.ts
Normal file
23
app/vmui/packages/vmui/config/plugins/dynamicIndexHtml.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { IndexHtmlTransform } from "vite";
|
||||
|
||||
/**
|
||||
* Vite plugin to dynamically load index.html based on the current mode.
|
||||
* If a specific mode-based index file (e.g., index.victorialogs.html) exists, it is used.
|
||||
* Otherwise, the default index.html is loaded.
|
||||
*/
|
||||
export default function dynamicIndexHtmlPlugin({ mode }) {
|
||||
return {
|
||||
name: "vm-dynamic-index-html",
|
||||
transformIndexHtml: {
|
||||
order: "pre",
|
||||
handler: async () => {
|
||||
try {
|
||||
return await readFile(`./index.${mode}.html`, "utf8");
|
||||
} catch (error) {
|
||||
return await readFile("./index.html", "utf8");
|
||||
}
|
||||
}
|
||||
} as IndexHtmlTransform
|
||||
};
|
||||
}
|
||||
90
app/vmui/packages/vmui/eslint.config.js
Normal file
90
app/vmui/packages/vmui/eslint.config.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import react from "eslint-plugin-react";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default [...compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
), {
|
||||
plugins: {
|
||||
react,
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
pragma: "React",
|
||||
version: "detect",
|
||||
},
|
||||
|
||||
linkComponents: ["Hyperlink", {
|
||||
name: "Link",
|
||||
linkAttribute: "to",
|
||||
}],
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-expressions": ["error", {
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true
|
||||
}],
|
||||
|
||||
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "none",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"destructuredArrayIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}],
|
||||
|
||||
"react/jsx-closing-bracket-location": [1, "line-aligned"],
|
||||
|
||||
"react/jsx-max-props-per-line": [1, {
|
||||
maximum: 1,
|
||||
}],
|
||||
|
||||
"react/jsx-first-prop-new-line": [1, "multiline"],
|
||||
"object-curly-spacing": [2, "always"],
|
||||
|
||||
indent: ["error", 2, {
|
||||
SwitchCase: 1,
|
||||
}],
|
||||
|
||||
"linebreak-style": ["error", "unix"],
|
||||
quotes: ["error", "double"],
|
||||
semi: ["error", "always"],
|
||||
"react/prop-types": 0,
|
||||
|
||||
},
|
||||
}];
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="mask-icon" href="%PUBLIC_URL%/favicon.svg" color="#000000">
|
||||
<link rel="icon" href="/favicon.svg"/>
|
||||
<link rel="apple-touch-icon" href="/favicon.svg"/>
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
@@ -13,24 +13,24 @@
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<link rel="manifest" href="/manifest.json"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>vmui</title>
|
||||
<script src="%PUBLIC_URL%/dashboards/index.js" type="module"></script>
|
||||
<script src="/dashboards/index.js" type="module"></script>
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="UI for VictoriaMetrics">
|
||||
<meta name="twitter:site" content="@https://victoriametrics.com/">
|
||||
<meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta name="twitter:image" content="/preview.jpg">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
@@ -50,5 +50,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="mask-icon" href="%PUBLIC_URL%/favicon.svg" color="#000000">
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
@@ -13,13 +13,13 @@
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<link rel="manifest" href="/manifest.json"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
@@ -29,7 +29,7 @@
|
||||
<meta name="twitter:title" content="UI for VictoriaLogs">
|
||||
<meta name="twitter:site" content="@https://victoriametrics.com/products/victorialogs/">
|
||||
<meta name="twitter:description" content="Explore your log data with VictoriaLogs UI">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta name="twitter:image" content="/preview.jpg">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="UI for VictoriaLogs">
|
||||
@@ -49,5 +49,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="mask-icon" href="%PUBLIC_URL%/favicon.svg" color="#000000">
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
@@ -13,13 +13,13 @@
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
|
||||
<link rel="manifest" href="/manifest.json"/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
@@ -29,7 +29,7 @@
|
||||
<meta name="twitter:title" content="UI for VictoriaMetrics Anomaly Detection">
|
||||
<meta name="twitter:site" content="@https://victoriametrics.com/products/enterprise/anomaly-detection/">
|
||||
<meta name="twitter:description" content="Detect anomalies in your metrics with VictoriaMetrics Anomaly Detection UI">
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/preview.jpg">
|
||||
<meta name="twitter:image" content="/preview.jpg">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="UI for VictoriaMetrics Anomaly Detection">
|
||||
@@ -49,5 +49,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
19985
app/vmui/packages/vmui/package-lock.json
generated
19985
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,50 +3,40 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "./",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/react-input-mask": "^3.0.5",
|
||||
"@types/qs": "^6.9.18",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-input-mask": "^3.0.6",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"classnames": "^2.5.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^14.1.2",
|
||||
"marked-emoji": "^1.4.2",
|
||||
"preact": "^10.23.2",
|
||||
"qs": "^6.13.0",
|
||||
"marked": "^15.0.6",
|
||||
"marked-emoji": "^1.4.3",
|
||||
"preact": "^10.25.4",
|
||||
"qs": "^6.14.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"sass": "^1.78.0",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "~4.6.2",
|
||||
"uplot": "^1.6.30",
|
||||
"web-vitals": "^4.2.3"
|
||||
"react-router-dom": "^7.1.5",
|
||||
"uplot": "^1.6.31",
|
||||
"vite": "^6.0.11",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "npm run copy-metricsql-docs",
|
||||
"start": "react-app-rewired start",
|
||||
"start:logs": "cross-env REACT_APP_TYPE=logs npm run start",
|
||||
"start:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run start",
|
||||
"build": "GENERATE_SOURCEMAP=false react-app-rewired build",
|
||||
"build:logs": "cross-env REACT_APP_TYPE=logs npm run build",
|
||||
"build:anomaly": "cross-env REACT_APP_TYPE=anomaly npm run build",
|
||||
"lint": "eslint src --ext tsx,ts",
|
||||
"lint:fix": "eslint src --ext tsx,ts --fix",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
"start": "vite",
|
||||
"start:logs": "vite --mode victorialogs",
|
||||
"start:anomaly": "vite --mode vmanomaly",
|
||||
"build": "vite build",
|
||||
"build:logs": "vite build --mode victorialogs",
|
||||
"build:anomaly": "vite build --mode vmanomaly",
|
||||
"lint": "eslint 'src/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
|
||||
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -61,26 +51,24 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.15.0",
|
||||
"@typescript-eslint/parser": "^5.15.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@preact/preset-vite": "^2.10.1",
|
||||
"@types/node": "^22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"customize-cra": "^1.0.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.36.1",
|
||||
"http-proxy-middleware": "^3.0.2",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"webpack": "^5.94.0"
|
||||
},
|
||||
"overrides": {
|
||||
"react-app-rewired": {
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"css-select": {
|
||||
"nth-check": "^2.0.1"
|
||||
}
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^15.14.0",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"postcss": "^8.5.1",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.83.4",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"typescript": "^5.7.3",
|
||||
"webpack": "^5.97.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,4 @@ const AppAnomaly: FC = () => {
|
||||
</>;
|
||||
};
|
||||
|
||||
export default AppAnomaly;
|
||||
export default AppAnomaly;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import uPlot from "uplot";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface MetricBase {
|
||||
group: number;
|
||||
metric: {
|
||||
@@ -6,13 +9,13 @@ export interface MetricBase {
|
||||
}
|
||||
|
||||
export interface MetricResult extends MetricBase {
|
||||
values: [number, string][]
|
||||
values: [number, string][];
|
||||
}
|
||||
|
||||
|
||||
export interface InstantMetricResult extends MetricBase {
|
||||
value?: [number, string]
|
||||
values?: [number, string][]
|
||||
value?: [number, string];
|
||||
values?: [number, string][];
|
||||
}
|
||||
|
||||
export interface ExportMetricResult extends MetricBase {
|
||||
@@ -43,10 +46,24 @@ export interface Logs {
|
||||
export interface LogHits {
|
||||
timestamps: string[];
|
||||
values: number[];
|
||||
total?: number;
|
||||
fields: {
|
||||
[key: string]: string;
|
||||
};
|
||||
total: number;
|
||||
fields: { [key: string]: string; };
|
||||
_isOther: boolean;
|
||||
}
|
||||
|
||||
export interface LegendLogHits {
|
||||
label: string;
|
||||
total: number;
|
||||
totalHits: number;
|
||||
isOther: boolean;
|
||||
fields: { [key: string]: string; };
|
||||
stroke?: uPlot.Series.Stroke;
|
||||
}
|
||||
|
||||
export interface LegendLogHitsMenu {
|
||||
title: string;
|
||||
icon?: ReactNode;
|
||||
handler?: () => void;
|
||||
}
|
||||
|
||||
export interface ReportMetaData {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useCallback, useMemo, useRef, useState } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
import useElementSize from "../../../hooks/useElementSize";
|
||||
import uPlot, { AlignedData } from "uplot";
|
||||
import { useEffect } from "react";
|
||||
import useBarHitsOptions from "./hooks/useBarHitsOptions";
|
||||
import useBarHitsOptions, { getLabelFromLogHit } from "./hooks/useBarHitsOptions";
|
||||
import BarHitsTooltip from "./BarHitsTooltip/BarHitsTooltip";
|
||||
import { TimeParams } from "../../../types";
|
||||
import usePlotScale from "../../../hooks/uplot/usePlotScale";
|
||||
import useReadyChart from "../../../hooks/uplot/useReadyChart";
|
||||
import useZoomChart from "../../../hooks/uplot/useZoomChart";
|
||||
import classNames from "classnames";
|
||||
import { LogHits } from "../../../api/types";
|
||||
import { LegendLogHits, LogHits } from "../../../api/types";
|
||||
import { addSeries, delSeries, setBand } from "../../../utils/uplot";
|
||||
import { GraphOptions, GRAPH_STYLES } from "./types";
|
||||
import BarHitsOptions from "./BarHitsOptions/BarHitsOptions";
|
||||
import stack from "../../../utils/uplot/stack";
|
||||
import BarHitsLegend from "./BarHitsLegend/BarHitsLegend";
|
||||
import { calculateTotalHits, sortLogHits } from "../../../utils/logs";
|
||||
|
||||
interface Props {
|
||||
logHits: LogHits[];
|
||||
@@ -57,6 +58,29 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
|
||||
graphOptions
|
||||
});
|
||||
|
||||
const prepareLegend = useCallback((hits: LogHits[], totalHits: number): LegendLogHits[] => {
|
||||
return hits.map((hit) => {
|
||||
const label = getLabelFromLogHit(hit);
|
||||
|
||||
const legendItem: LegendLogHits = {
|
||||
label,
|
||||
isOther: hit._isOther,
|
||||
fields: hit.fields,
|
||||
total: hit.total || 0,
|
||||
totalHits,
|
||||
stroke: series.find((s) => s.label === label)?.stroke,
|
||||
};
|
||||
|
||||
return legendItem;
|
||||
}).sort(sortLogHits("total"));
|
||||
}, [series]);
|
||||
|
||||
|
||||
const legendDetails: LegendLogHits[] = useMemo(() => {
|
||||
const totalHits = calculateTotalHits(logHits);
|
||||
return prepareLegend(logHits, totalHits);
|
||||
}, [logHits, prepareLegend]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
delSeries(uPlotInst);
|
||||
@@ -121,6 +145,7 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
|
||||
<BarHitsLegend
|
||||
uPlotInst={uPlotInst}
|
||||
onApplyFilter={onApplyFilter}
|
||||
legendDetails={legendDetails}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,83 +1,53 @@
|
||||
import React, { FC, useCallback, useEffect, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useState } from "preact/compat";
|
||||
import uPlot, { Series } from "uplot";
|
||||
import "./style.scss";
|
||||
import "../../Line/Legend/style.scss";
|
||||
import classNames from "classnames";
|
||||
import { MouseEvent } from "react";
|
||||
import { isMacOs } from "../../../../utils/detect-device";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import { getStreamPairs } from "../../../../utils/logs";
|
||||
import BarHitsLegendItem from "./BarHitsLegendItem";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
|
||||
interface Props {
|
||||
uPlotInst: uPlot;
|
||||
legendDetails: LegendLogHits[];
|
||||
onApplyFilter: (value: string) => void;
|
||||
}
|
||||
|
||||
const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
|
||||
const BarHitsLegend: FC<Props> = ({ uPlotInst, legendDetails, onApplyFilter }) => {
|
||||
const [series, setSeries] = useState<Series[]>([]);
|
||||
const [pairs, setPairs] = useState<string[][]>([]);
|
||||
const totalHits = legendDetails[0]?.totalHits || 0;
|
||||
|
||||
const updateSeries = useCallback(() => {
|
||||
const series = uPlotInst.series.filter(s => s.scale !== "x");
|
||||
setSeries(series);
|
||||
setPairs(series.map(s => getStreamPairs(s.label || "")));
|
||||
const getSeries = () => {
|
||||
return uPlotInst.series.filter(s => s.scale !== "x");
|
||||
};
|
||||
|
||||
const handleRedrawGraph = () => {
|
||||
uPlotInst.redraw();
|
||||
setSeries(getSeries());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSeries(getSeries());
|
||||
}, [uPlotInst]);
|
||||
|
||||
const handleClickByValue = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const metaKey = e.metaKey || e.ctrlKey;
|
||||
if (!metaKey) return;
|
||||
onApplyFilter(`{${value}}` || "");
|
||||
updateSeries();
|
||||
uPlotInst.redraw();
|
||||
};
|
||||
|
||||
const handleClickByStream = (target: Series) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const metaKey = e.metaKey || e.ctrlKey;
|
||||
if (metaKey) return;
|
||||
target.show = !target.show;
|
||||
updateSeries();
|
||||
uPlotInst.redraw();
|
||||
};
|
||||
|
||||
useEffect(updateSeries, [uPlotInst]);
|
||||
|
||||
return (
|
||||
<div className="vm-bar-hits-legend">
|
||||
{series.map((s, i) => (
|
||||
<Tooltip
|
||||
key={s.label}
|
||||
title={(
|
||||
<ul className="vm-bar-hits-legend-info">
|
||||
<li>Click to {s.show ? "hide" : "show"} the _stream.</li>
|
||||
<li>{isMacOs() ? "Cmd" : "Ctrl"} + Click to filter by the _stream.</li>
|
||||
</ul>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-bar-hits-legend-item": true,
|
||||
"vm-bar-hits-legend-item_hide": !s.show,
|
||||
})}
|
||||
onClick={handleClickByStream(s)}
|
||||
>
|
||||
<div
|
||||
className="vm-bar-hits-legend-item__marker"
|
||||
style={{ backgroundColor: `${(s?.stroke as () => string)?.()}` }}
|
||||
/>
|
||||
<div className="vm-bar-hits-legend-item-pairs">
|
||||
{pairs[i].map(value => (
|
||||
<span
|
||||
className="vm-bar-hits-legend-item-pairs__value"
|
||||
key={value}
|
||||
onClick={handleClickByValue(value)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{legendDetails.map((legend) => (
|
||||
<BarHitsLegendItem
|
||||
key={legend.label}
|
||||
legend={legend}
|
||||
series={series}
|
||||
onRedrawGraph={handleRedrawGraph}
|
||||
onApplyFilter={onApplyFilter}
|
||||
/>
|
||||
))}
|
||||
<div className="vm-bar-hits-legend-info">
|
||||
<div>
|
||||
Total hits: <b>{totalHits.toLocaleString("en-US")}</b>
|
||||
</div>
|
||||
<div>
|
||||
<code>L-Click</code> toggles visibility.
|
||||
<code>R-Click</code> opens menu.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { Series } from "uplot";
|
||||
import { MouseEvent } from "react";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
import { getStreamPairs } from "../../../../utils/logs";
|
||||
import { formatNumberShort } from "../../../../utils/math";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import useBoolean from "../../../../hooks/useBoolean";
|
||||
import LegendHitsMenu from "../LegendHitsMenu/LegendHitsMenu";
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
series: Series[];
|
||||
onRedrawGraph: () => void;
|
||||
onApplyFilter: (value: string) => void;
|
||||
}
|
||||
|
||||
const BarHitsLegendItem: FC<Props> = ({ legend, series, onRedrawGraph, onApplyFilter }) => {
|
||||
const {
|
||||
value: openContextMenu,
|
||||
setTrue: handleOpenContextMenu,
|
||||
setFalse: handleCloseContextMenu,
|
||||
} = useBoolean(false);
|
||||
|
||||
const legendRef = useRef<HTMLDivElement>(null);
|
||||
const [clickPosition, setClickPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
const targetSeries = useMemo(() => series.find(s => s.label === legend.label), [series]);
|
||||
|
||||
const fields = useMemo(() => getStreamPairs(legend.label), [legend.label]);
|
||||
|
||||
const label = fields.join(", ");
|
||||
const totalShortFormatted = formatNumberShort(legend.total);
|
||||
|
||||
const handleClickByStream = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!targetSeries) return;
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
targetSeries.show = !targetSeries.show;
|
||||
} else {
|
||||
const isOnlyTargetVisible = series.every(s => s === targetSeries || !s.show);
|
||||
series.forEach(s => {
|
||||
s.show = isOnlyTargetVisible || (s === targetSeries);
|
||||
});
|
||||
}
|
||||
|
||||
onRedrawGraph();
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setClickPosition({ top: e.clientY, left: e.clientX });
|
||||
handleOpenContextMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={legendRef}
|
||||
className={classNames({
|
||||
"vm-bar-hits-legend-item": true,
|
||||
"vm-bar-hits-legend-item_other": legend.isOther,
|
||||
"vm-bar-hits-legend-item_hide": !targetSeries?.show,
|
||||
})}
|
||||
onClick={handleClickByStream}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<div
|
||||
className="vm-bar-hits-legend-item__marker"
|
||||
style={{ backgroundColor: `${legend.stroke}` }}
|
||||
/>
|
||||
<div className="vm-bar-hits-legend-item__label">{label}</div>
|
||||
<span className="vm-bar-hits-legend-item__total">({totalShortFormatted})</span>
|
||||
<Popper
|
||||
placement="fixed"
|
||||
open={openContextMenu}
|
||||
buttonRef={legendRef}
|
||||
placementPosition={clickPosition}
|
||||
onClose={handleCloseContextMenu}
|
||||
>
|
||||
<LegendHitsMenu
|
||||
legend={legend}
|
||||
fields={fields}
|
||||
onApplyFilter={onApplyFilter}
|
||||
onClose={handleCloseContextMenu}
|
||||
/>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarHitsLegendItem;
|
||||
@@ -3,16 +3,16 @@
|
||||
.vm-bar-hits-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
padding: 0 $padding-small $padding-small;
|
||||
color: $color-text;
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
max-width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
font-size: 12px;
|
||||
padding: 0 $padding-small;
|
||||
font-size: $font-size-small;
|
||||
padding: $padding-small $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
@@ -27,34 +27,44 @@
|
||||
}
|
||||
|
||||
&__marker {
|
||||
width: 14px;
|
||||
min-width: 14px;
|
||||
max-width: 14px;
|
||||
height: 14px;
|
||||
border: $color-background-block;
|
||||
}
|
||||
|
||||
&-pairs {
|
||||
display: flex;
|
||||
gap: $padding-small;
|
||||
&__label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__value {
|
||||
padding: $padding-small 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ",";
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
&__total {
|
||||
color: $color-text-secondary;
|
||||
font-style: italic;
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
list-style-position: inside;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: $padding-small;
|
||||
color: $color-text-secondary;
|
||||
font-size: $font-size-small;
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
padding: calc($padding-small / 2) $padding-small;
|
||||
font-size: $font-size-small;
|
||||
text-align: center;
|
||||
background-color: $color-background-body;
|
||||
background-repeat: repeat-x;
|
||||
border: $border-divider;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import "./style.scss";
|
||||
import useStateSearchParams from "../../../../hooks/useStateSearchParams";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import Button from "../../../Main/Button/Button";
|
||||
import classNames from "classnames";
|
||||
import { SettingsIcon, VisibilityIcon, VisibilityOffIcon } from "../../../Main/Icons";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
@@ -24,27 +23,20 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
|
||||
setFalse: handleCloseOptions,
|
||||
} = useBoolean(false);
|
||||
|
||||
const [graphStyle, setGraphStyle] = useStateSearchParams(GRAPH_STYLES.LINE_STEPPED, "graph");
|
||||
const [stacked, setStacked] = useStateSearchParams(false, "stacked");
|
||||
const [fill, setFill] = useStateSearchParams(false, "fill");
|
||||
const [fill, setFill] = useStateSearchParams("true", "fill");
|
||||
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
|
||||
|
||||
const options: GraphOptions = useMemo(() => ({
|
||||
graphStyle,
|
||||
graphStyle: GRAPH_STYLES.BAR,
|
||||
stacked,
|
||||
fill,
|
||||
fill: fill === "true",
|
||||
hideChart,
|
||||
}), [graphStyle, stacked, fill, hideChart]);
|
||||
|
||||
const handleChangeGraphStyle = (val: string) => () => {
|
||||
setGraphStyle(val as GRAPH_STYLES);
|
||||
searchParams.set("graph", val);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
}), [stacked, fill, hideChart]);
|
||||
|
||||
const handleChangeFill = (val: boolean) => {
|
||||
setFill(val);
|
||||
val ? searchParams.set("fill", "true") : searchParams.delete("fill");
|
||||
setFill(`${val}`);
|
||||
searchParams.set("fill", `${val}`);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
@@ -97,21 +89,6 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
|
||||
title={"Graph settings"}
|
||||
>
|
||||
<div className="vm-bar-hits-options-settings">
|
||||
<div className="vm-bar-hits-options-settings-item vm-bar-hits-options-settings-item_list">
|
||||
<p className="vm-bar-hits-options-settings-item__title">Graph style:</p>
|
||||
{Object.values(GRAPH_STYLES).map(style => (
|
||||
<div
|
||||
key={style}
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_active": graphStyle === style,
|
||||
})}
|
||||
onClick={handleChangeGraphStyle(style)}
|
||||
>
|
||||
{style}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="vm-bar-hits-options-settings-item">
|
||||
<Switch
|
||||
label={"Stacked"}
|
||||
@@ -122,7 +99,7 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
|
||||
<div className="vm-bar-hits-options-settings-item">
|
||||
<Switch
|
||||
label={"Fill"}
|
||||
value={fill}
|
||||
value={fill === "true"}
|
||||
onChange={handleChangeFill}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
&-settings {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
gap: $padding-global;
|
||||
min-width: 200px;
|
||||
gap: $padding-global;
|
||||
padding-bottom: $padding-global;
|
||||
|
||||
&-item {
|
||||
border-bottom: $border-divider;
|
||||
padding: 0 $padding-global $padding-global;
|
||||
padding: 0 $padding-global;
|
||||
|
||||
&_list {
|
||||
padding: 0;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
import "../../ChartTooltip/style.scss";
|
||||
import { sortLogHits } from "../../../../utils/logs";
|
||||
|
||||
interface Props {
|
||||
data: AlignedData;
|
||||
@@ -26,7 +27,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
|
||||
const tooltipItems = values.map((value, i) => {
|
||||
const targetSeries = series[i + 1];
|
||||
const stroke = (targetSeries?.stroke as () => string)?.();
|
||||
const label = targetSeries?.label || "other";
|
||||
const label = targetSeries?.label;
|
||||
const show = targetSeries?.show;
|
||||
return {
|
||||
label,
|
||||
@@ -34,7 +35,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
|
||||
value,
|
||||
show
|
||||
};
|
||||
}).filter(item => item.value > 0 && item.show).sort((a, b) => b.value - a.value);
|
||||
}).filter(item => item.value > 0 && item.show).sort(sortLogHits("value"));
|
||||
|
||||
const point = {
|
||||
top: tooltipItems[0] ? uPlotInst?.valToPos?.(tooltipItems[0].value, "y") || 0 : 0,
|
||||
@@ -104,21 +105,24 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
|
||||
className="vm-chart-tooltip-data__marker"
|
||||
style={{ background: item.stroke }}
|
||||
/>
|
||||
<p>
|
||||
{item.label}: <b>{item.value}</b>
|
||||
<p className="vm-bar-hits-tooltip-item">
|
||||
<span className="vm-bar-hits-tooltip-item__label">{item.label}</span>
|
||||
<span>{item.value.toLocaleString("en-US")}</span>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{tooltipData.values.length > 1 && (
|
||||
<div className="vm-chart-tooltip-data">
|
||||
<p>
|
||||
Total records: <b>{tooltipData.total}</b>
|
||||
<span/>
|
||||
<p className="vm-bar-hits-tooltip-item">
|
||||
<span className="vm-bar-hits-tooltip-item__label">Total</span>
|
||||
<span>{tooltipData.total.toLocaleString("en-US")}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__title">
|
||||
<div className="vm-chart-tooltip-header__title vm-bar-hits-tooltip__date">
|
||||
{tooltipData.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,4 +9,23 @@
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: $padding-global;
|
||||
max-width: 100%;
|
||||
|
||||
&__label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__date {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
import LegendHitsMenuStats from "./LegendHitsMenuStats";
|
||||
import LegendHitsMenuBase from "./LegendHitsMenuBase";
|
||||
import LegendHitsMenuRow from "./LegendHitsMenuRow";
|
||||
import LegendHitsMenuFields from "./LegendHitsMenuFields";
|
||||
import { LOGS_LIMIT_HITS } from "../../../../constants/logs";
|
||||
|
||||
const otherDescription = `aggregated results for fields not in the top ${LOGS_LIMIT_HITS}`;
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
fields: string[];
|
||||
onApplyFilter: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LegendHitsMenu: FC<Props> = ({ legend, fields, onApplyFilter, onClose }) => {
|
||||
return (
|
||||
<div className="vm-legend-hits-menu">
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
<LegendHitsMenuRow
|
||||
className="vm-legend-hits-menu-row_info"
|
||||
title={legend.isOther ? otherDescription : legend.label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!legend.isOther && (
|
||||
<LegendHitsMenuBase
|
||||
legend={legend}
|
||||
onApplyFilter={onApplyFilter}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!legend.isOther && (
|
||||
<LegendHitsMenuFields
|
||||
fields={fields}
|
||||
onApplyFilter={onApplyFilter}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LegendHitsMenuStats legend={legend}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenu;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import LegendHitsMenuRow from "./LegendHitsMenuRow";
|
||||
import useCopyToClipboard from "../../../../hooks/useCopyToClipboard";
|
||||
import { CopyIcon, FilterIcon, FilterOffIcon } from "../../../Main/Icons";
|
||||
import { LegendLogHits, LegendLogHitsMenu } from "../../../../api/types";
|
||||
import { LOGS_GROUP_BY } from "../../../../constants/logs";
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
onApplyFilter: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LegendHitsMenuBase: FC<Props> = ({ legend, onApplyFilter, onClose }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const handleAddStreamToFilter = () => {
|
||||
onApplyFilter(`${LOGS_GROUP_BY}: ${legend.label}`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleExcludeStreamToFilter = () => {
|
||||
onApplyFilter(`(NOT ${LOGS_GROUP_BY}: ${legend.label})`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handlerCopyLabel = async () => {
|
||||
await copyToClipboard(legend.label, `${legend.label} has been copied`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const options: LegendLogHitsMenu[] = [
|
||||
{
|
||||
title: `Copy ${LOGS_GROUP_BY} name`,
|
||||
icon: <CopyIcon/>,
|
||||
handler: handlerCopyLabel,
|
||||
},
|
||||
{
|
||||
title: `Add ${LOGS_GROUP_BY} to filter`,
|
||||
icon: <FilterIcon/>,
|
||||
handler: handleAddStreamToFilter,
|
||||
},
|
||||
{
|
||||
title: `Exclude ${LOGS_GROUP_BY} to filter`,
|
||||
icon: <FilterOffIcon/>,
|
||||
handler: handleExcludeStreamToFilter,
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
{options.map(({ icon, title, handler }) => (
|
||||
<LegendHitsMenuRow
|
||||
key={title}
|
||||
iconStart={icon}
|
||||
title={title}
|
||||
handler={handler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuBase;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import LegendHitsMenuRow from "./LegendHitsMenuRow";
|
||||
import { CopyIcon, FilterIcon, FilterOffIcon } from "../../../Main/Icons";
|
||||
import { convertToFieldFilter } from "../../../../utils/logs";
|
||||
import { LegendLogHitsMenu } from "../../../../api/types";
|
||||
import useCopyToClipboard from "../../../../hooks/useCopyToClipboard";
|
||||
|
||||
interface Props {
|
||||
fields: string[];
|
||||
onApplyFilter: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LegendHitsMenuFields: FC<Props> = ({ fields, onApplyFilter, onClose }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
|
||||
const handleCopy = (field: string) => async () => {
|
||||
await copyToClipboard(field, `${field} has been copied`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleAddToFilter = (field: string) => () => {
|
||||
onApplyFilter(field);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleExcludeToFilter = (field: string) => () => {
|
||||
onApplyFilter(`-${field}`);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const generateFieldMenu = (field: string): LegendLogHitsMenu[] => {
|
||||
return [
|
||||
{
|
||||
title: "Copy",
|
||||
icon: <CopyIcon/>,
|
||||
handler: handleCopy(field),
|
||||
},
|
||||
{
|
||||
title: "Add to filter",
|
||||
icon: <FilterIcon/>,
|
||||
handler: handleAddToFilter(field),
|
||||
},
|
||||
{
|
||||
title: "Exclude to filter",
|
||||
icon: <FilterOffIcon/>,
|
||||
handler: handleExcludeToFilter(field),
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const fieldsWithMenu: LegendLogHitsMenu[] = useMemo(() => {
|
||||
return fields.map(field => {
|
||||
const title = convertToFieldFilter(field);
|
||||
return {
|
||||
title,
|
||||
submenu: generateFieldMenu(title),
|
||||
};
|
||||
});
|
||||
}, [fields]);
|
||||
|
||||
return (
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
{fieldsWithMenu?.map((field) => (
|
||||
<LegendHitsMenuRow
|
||||
key={field.title}
|
||||
{...field}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuFields;
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, { FC, useRef, useState } from "preact/compat";
|
||||
import classNames from "classnames";
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import { LegendLogHitsMenu } from "../../../../api/types";
|
||||
import { ArrowDropDownIcon } from "../../../Main/Icons";
|
||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||
|
||||
interface Props {
|
||||
title: string | ReactNode;
|
||||
handler?: () => void;
|
||||
iconStart?: ReactNode;
|
||||
iconEnd?: ReactNode;
|
||||
className?: string;
|
||||
submenu?: LegendLogHitsMenu[];
|
||||
}
|
||||
|
||||
const LegendHitsMenuRow: FC<Props> = ({ title, handler, iconStart, iconEnd, className, submenu }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const titleRef = useRef<HTMLDivElement>(null);
|
||||
const submenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isOverflownTitle, setIsOverflownTitle] = useState(false);
|
||||
|
||||
const [openSubmenu, setOpenSubmenu] = useState(false);
|
||||
const [posSubmenuLeft, setPosSubmenuLeft] = useState(false);
|
||||
const hasSubmenu = !!submenu?.length;
|
||||
|
||||
const handleToggleContextMenu = () => {
|
||||
setOpenSubmenu(prev => !prev);
|
||||
};
|
||||
|
||||
const handleCloseContextMenu = () => {
|
||||
setOpenSubmenu(false);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
handler && handler();
|
||||
hasSubmenu && handleToggleContextMenu();
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!titleRef.current) return;
|
||||
setIsOverflownTitle(titleRef.current.scrollWidth > titleRef.current.clientWidth);
|
||||
}, [title, titleRef]);
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!openSubmenu || !submenuRef.current) {
|
||||
setPosSubmenuLeft(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { left, width } = submenuRef.current.getBoundingClientRect();
|
||||
setPosSubmenuLeft(left + width > window.innerWidth);
|
||||
});
|
||||
}, [submenuRef, openSubmenu]);
|
||||
|
||||
useClickOutside(containerRef, handleCloseContextMenu);
|
||||
|
||||
const titleContent = (
|
||||
<div
|
||||
ref={titleRef}
|
||||
className="vm-legend-hits-menu-row__title"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames({
|
||||
"vm-legend-hits-menu-row": true,
|
||||
"vm-legend-hits-menu-row_interactive": !!handler || hasSubmenu,
|
||||
[`${className}`]: className
|
||||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{iconStart && <div className="vm-legend-hits-menu-row__icon">{iconStart}</div>}
|
||||
{isOverflownTitle ? (<Tooltip title={title}>{titleContent}</Tooltip>) : titleContent}
|
||||
{iconEnd && !hasSubmenu && <div className="vm-legend-hits-menu-row__icon">{iconEnd}</div>}
|
||||
|
||||
{hasSubmenu && (
|
||||
<div className="vm-legend-hits-menu-row__icon vm-legend-hits-menu-row__icon_drop">
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openSubmenu && submenu && (
|
||||
<div
|
||||
ref={submenuRef}
|
||||
className={classNames({
|
||||
"vm-legend-hits-menu": true,
|
||||
"vm-legend-hits-menu_submenu": true,
|
||||
"vm-legend-hits-menu_submenu_left": posSubmenuLeft
|
||||
})}
|
||||
>
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
{submenu.map(({ icon, title, handler }) => (
|
||||
<LegendHitsMenuRow
|
||||
key={title}
|
||||
iconStart={icon}
|
||||
title={title}
|
||||
handler={handler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuRow;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import { LegendLogHits } from "../../../../api/types";
|
||||
|
||||
interface Props {
|
||||
legend: LegendLogHits;
|
||||
}
|
||||
|
||||
const LegendHitsMenuStats: FC<Props> = ({ legend }) => {
|
||||
const totalFormatted = legend.total.toLocaleString("en-US");
|
||||
const percentage = Math.round((legend.total / legend.totalHits) * 100);
|
||||
|
||||
return (
|
||||
<div className="vm-legend-hits-menu-section">
|
||||
<div className="vm-legend-hits-menu-row">
|
||||
<div className="vm-legend-hits-menu-row__title">
|
||||
Total: {totalFormatted} ({percentage}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendHitsMenuStats;
|
||||
@@ -0,0 +1,178 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-legend-hits-menu {
|
||||
min-width: 160px;
|
||||
z-index: 1;
|
||||
|
||||
&_submenu {
|
||||
position: absolute;
|
||||
top: calc(-1 * $padding-small);
|
||||
background-color: $color-background-block;
|
||||
left: calc(100% + ($padding-small / 2));
|
||||
box-shadow: $box-shadow-popper;
|
||||
border-radius: $border-radius-small;
|
||||
animation: vm-submenu-show 150ms cubic-bezier(0.280, 0.840, 0.2, 1);
|
||||
transform-origin: top left;
|
||||
|
||||
&_left {
|
||||
left: auto;
|
||||
right: calc(100% + ($padding-small / 2));
|
||||
transform-origin: top right;
|
||||
}
|
||||
}
|
||||
|
||||
&-section {
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 $padding-global;
|
||||
transition: background-color 0.3s;
|
||||
color: $color-text;
|
||||
|
||||
&_interactive {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
&_info {
|
||||
font-size: $font-size-small;
|
||||
font-weight: 500;
|
||||
padding-block: $padding-small;
|
||||
}
|
||||
|
||||
&_info &__icon {
|
||||
color: $color-info;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
|
||||
&_drop {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex-grow: 1;
|
||||
padding: $padding-global 0;
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&-other-list {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
overflow: auto;
|
||||
|
||||
&__search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: $padding-small 0;
|
||||
background-color: $color-background-block;
|
||||
border-bottom: $border-divider;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&-row {
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&_header {
|
||||
border-bottom: none;
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
background-color: $color-background-block;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-bottom: $border-divider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-cell {
|
||||
padding: calc($padding-small / 2) 0;
|
||||
text-align: left;
|
||||
|
||||
&_header {
|
||||
padding: $padding-small;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&_number {
|
||||
padding: $padding-small;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&_fields {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
&__field {
|
||||
padding: calc($padding-small / 2) $padding-small;
|
||||
border-radius: $border-radius-small;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
&:after {
|
||||
content: ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vm-submenu-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,14 @@ interface UseGetBarHitsOptionsArgs {
|
||||
graphOptions: GraphOptions;
|
||||
}
|
||||
|
||||
export const OTHER_HITS_LABEL = "other";
|
||||
|
||||
export const getLabelFromLogHit = (logHit: LogHits) => {
|
||||
if (logHit?._isOther) return OTHER_HITS_LABEL;
|
||||
const fields = Object.values(logHit?.fields || {});
|
||||
return fields.map((value) => value || "\"\"").join(", ");
|
||||
};
|
||||
|
||||
const useBarHitsOptions = ({
|
||||
data,
|
||||
logHits,
|
||||
@@ -59,16 +67,16 @@ const useBarHitsOptions = ({
|
||||
let colorN = 0;
|
||||
return data.map((_d, i) => {
|
||||
if (i === 0) return {}; // 0 index is xAxis(timestamps)
|
||||
const fields = Object.values(logHits?.[i - 1]?.fields || {});
|
||||
const label = fields.map((value) => value || "\"\"").join(", ");
|
||||
const color = getCssVariable(label ? seriesColors[colorN] : "color-log-hits-bar-0");
|
||||
if (label) colorN++;
|
||||
const target = logHits?.[i - 1];
|
||||
const label = getLabelFromLogHit(target);
|
||||
const color = getCssVariable(target?._isOther ? "color-log-hits-bar-0" : seriesColors[colorN]);
|
||||
if (!target?._isOther) colorN++;
|
||||
return {
|
||||
label: label || "other",
|
||||
label,
|
||||
width: strokeWidth[graphOptions.graphStyle],
|
||||
spanGaps: true,
|
||||
stroke: color,
|
||||
fill: graphOptions.fill ? color + "80" : "",
|
||||
fill: graphOptions.fill ? color + (target?._isOther ? "" : "80") : "",
|
||||
paths: getSeriesPaths(graphOptions.graphStyle),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useCallback, useEffect, useRef, useState, createPortal } from "preact/compat";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import uPlot from "uplot";
|
||||
import Button from "../../Main/Button/Button";
|
||||
@@ -49,7 +48,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
onClose && onClose(id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: ReactMouseEvent) => {
|
||||
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement>) => {
|
||||
setMoved(true);
|
||||
setMoving(true);
|
||||
const { clientX, clientY } = e;
|
||||
@@ -107,7 +106,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
|
||||
if (!u) return null;
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
return createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-chart-tooltip": true,
|
||||
|
||||
@@ -32,6 +32,11 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
|
||||
max-width: calc(100vw/3);
|
||||
}
|
||||
|
||||
&_hits &-data {
|
||||
display: grid;
|
||||
grid-template-columns: $font-size 1fr;
|
||||
}
|
||||
|
||||
&_sticky {
|
||||
pointer-events: auto;
|
||||
z-index: 99;
|
||||
@@ -90,6 +95,8 @@ $chart-tooltip-y: -1 * ($padding-global + $chart-tooltip-half-icon);
|
||||
}
|
||||
|
||||
&__marker {
|
||||
min-width: $font-size;
|
||||
max-width: $font-size;
|
||||
width: $font-size;
|
||||
height: $font-size;
|
||||
border: 1px solid rgba($color-white, 0.5);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use "src/styles/variables" as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$color-bar: #33BB55;
|
||||
$color-bar-highest: #F79420;
|
||||
@@ -7,7 +8,7 @@ $color-bar-highest: #F79420;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
height: 100%;
|
||||
padding-bottom: calc($font-size-small/2);
|
||||
padding-bottom: calc($font-size-small / 2);
|
||||
overflow: hidden;
|
||||
|
||||
&-y-axis {
|
||||
@@ -54,19 +55,19 @@ $color-bar-highest: #F79420;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
height: calc(100% - ($font-size-small*4));
|
||||
height: calc(100% - ($font-size-small * 4));
|
||||
background-color: $color-bar;
|
||||
transition: background-color 200ms ease-in;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-bar, 10%);
|
||||
background-color: color.scale($color-bar, $lightness: 40%);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
background-color: $color-bar-highest;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-bar-highest, 10%);
|
||||
background-color: color.scale($color-bar-highest, $lightness: 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,11 @@ import Timezones from "./Timezones/Timezones";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import { AppType } from "../../../types/appType";
|
||||
import SwitchMarkdownParsing from "../LogsSettings/MarkdownParsing/SwitchMarkdownParsing";
|
||||
import { APP_TYPE_LOGS } from "../../../constants/appType";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
const { REACT_APP_TYPE } = process.env;
|
||||
const isLogsApp = REACT_APP_TYPE === AppType.logs;
|
||||
|
||||
export interface ChildComponentHandle {
|
||||
handleApply: () => void;
|
||||
}
|
||||
@@ -48,21 +45,21 @@ const GlobalSettings: FC = () => {
|
||||
|
||||
const controls = [
|
||||
{
|
||||
show: !appModeEnable && !isLogsApp,
|
||||
show: !appModeEnable && !APP_TYPE_LOGS,
|
||||
component: <ServerConfigurator
|
||||
ref={serverSettingRef}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
},
|
||||
{
|
||||
show: !isLogsApp,
|
||||
show: !APP_TYPE_LOGS,
|
||||
component: <LimitsConfigurator
|
||||
ref={limitsSettingRef}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
},
|
||||
{
|
||||
show: isLogsApp,
|
||||
show: APP_TYPE_LOGS,
|
||||
component: <SwitchMarkdownParsing/>
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,35 +36,40 @@ const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYax
|
||||
"vm-axes-limits_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
<Switch
|
||||
value={yaxis.limits.enable}
|
||||
onChange={toggleEnableLimits}
|
||||
label="Fix the limits for y-axis"
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
<div className="vm-axes-limits-list">
|
||||
{axes.map(axis => (
|
||||
<div
|
||||
className="vm-axes-limits-list__inputs"
|
||||
key={axis}
|
||||
>
|
||||
<TextField
|
||||
label={`Min ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][0]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 0)}
|
||||
/>
|
||||
<TextField
|
||||
label={`Max ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][1]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 1)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Fixed Y-axis limits</span>
|
||||
<Switch
|
||||
value={yaxis.limits.enable}
|
||||
onChange={toggleEnableLimits}
|
||||
label={`${yaxis.limits.enable ? "Fixed" : "Auto"} limits`}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
{yaxis.limits.enable && (
|
||||
<div className="vm-axes-limits-list">
|
||||
{axes.map(axis => (
|
||||
<div
|
||||
className="vm-axes-limits-list__inputs"
|
||||
key={axis}
|
||||
>
|
||||
<TextField
|
||||
label={`Min ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][0]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 0)}
|
||||
/>
|
||||
<TextField
|
||||
label={`Max ${axis}`}
|
||||
type="number"
|
||||
disabled={!yaxis.limits.enable}
|
||||
value={yaxis.limits.range[axis][1]}
|
||||
onChange={createHandlerOnchangeAxis(axis, 1)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,10 +8,14 @@ import "./style.scss";
|
||||
import Tooltip from "../../Main/Tooltip/Tooltip";
|
||||
import useBoolean from "../../../hooks/useBoolean";
|
||||
import LinesConfigurator from "./LinesConfigurator/LinesConfigurator";
|
||||
import GraphTypeSwitcher from "./GraphTypeSwitcher/GraphTypeSwitcher";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { isHistogramData } from "../../../utils/metric";
|
||||
|
||||
const title = "Graph settings";
|
||||
|
||||
interface GraphSettingsProps {
|
||||
data: MetricResult[],
|
||||
yaxis: YaxisState,
|
||||
setYaxisLimits: (limits: AxisRange) => void,
|
||||
toggleEnableLimits: () => void,
|
||||
@@ -19,11 +23,13 @@ interface GraphSettingsProps {
|
||||
value: boolean,
|
||||
onChange: (value: boolean) => void,
|
||||
},
|
||||
isHistogram?: boolean,
|
||||
}
|
||||
|
||||
const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits, spanGaps }) => {
|
||||
const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, toggleEnableLimits, spanGaps }) => {
|
||||
const popperRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const displayHistogramMode = isHistogramData(data);
|
||||
|
||||
const {
|
||||
value: openPopper,
|
||||
@@ -64,6 +70,7 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
|
||||
spanGaps={spanGaps.value}
|
||||
onChange={spanGaps.onChange}
|
||||
/>
|
||||
{displayHistogramMode && <GraphTypeSwitcher onChange={handleClose}/>}
|
||||
</div>
|
||||
</div>
|
||||
</Popper>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import Switch from "../../../Main/Switch/Switch";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useChangeDisplayMode } from "./useChangeDisplayMode";
|
||||
|
||||
type Props = {
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
const GraphTypeSwitcher: FC<Props> = ({ onChange }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { handleChange } = useChangeDisplayMode();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const value = !searchParams.get("display_mode");
|
||||
|
||||
const handleChangeMode = (val: boolean) => {
|
||||
handleChange(val, onChange);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Histogram mode</span>
|
||||
<Switch
|
||||
value={value}
|
||||
onChange={handleChangeMode}
|
||||
label={value ? "Enabled" : "Disabled"}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphTypeSwitcher;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export const useChangeDisplayMode = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const dispatch = useTimeDispatch();
|
||||
|
||||
const handleChange = (val: boolean, callback?: () => void) => {
|
||||
val ? searchParams.delete("display_mode") : searchParams.set("display_mode", "lines");
|
||||
setSearchParams(searchParams);
|
||||
dispatch({ type: "RUN_QUERY" });
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
return { handleChange };
|
||||
};
|
||||
@@ -10,14 +10,17 @@ interface Props {
|
||||
const LinesConfigurator: FC<Props> = ({ spanGaps, onChange }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
return <div>
|
||||
<Switch
|
||||
value={spanGaps}
|
||||
onChange={onChange}
|
||||
label="Connect null values"
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>;
|
||||
return (
|
||||
<div className="vm-graph-settings-row">
|
||||
<span className="vm-graph-settings-row__label">Connect null values</span>
|
||||
<Switch
|
||||
value={spanGaps}
|
||||
onChange={onChange}
|
||||
label={spanGaps ? "Enabled" : "Disabled"}
|
||||
fullWidth={isMobile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinesConfigurator;
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-graph-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
|
||||
&-popper {
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
padding: 0 0 $padding-global;
|
||||
padding: $padding-small $padding-large $padding-large;
|
||||
min-width: 300px;
|
||||
|
||||
&__body {
|
||||
display: grid;
|
||||
gap: $padding-large;
|
||||
padding: 0 $padding-global;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
grid-template-columns: minmax(150px, max-content) 1fr;
|
||||
|
||||
&__label {
|
||||
&:after{
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class QueryAutocompleteCache {
|
||||
put(key: QueryAutocompleteCacheItem, value: string[]) {
|
||||
if (this.map.size >= this.maxSize) {
|
||||
const firstKey = this.map.keys().next().value;
|
||||
this.map.delete(firstKey);
|
||||
firstKey && this.map.delete(firstKey);
|
||||
}
|
||||
this.map.set(JSON.stringify(key), value);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import { KeyboardEvent } from "react";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
import TextField from "../../Main/TextField/TextField";
|
||||
import TextField, { TextFieldKeyboardEvent } from "../../Main/TextField/TextField";
|
||||
import "./style.scss";
|
||||
import { QueryStats } from "../../../api/types";
|
||||
import { partialWarning, seriesFetchedWarning } from "./warningText";
|
||||
@@ -81,7 +80,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
setCaretPositionInput([caretPosition, caretPosition]);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const handleKeyDown = (e: TextFieldKeyboardEvent) => {
|
||||
const { key, ctrlKey, metaKey, shiftKey } = e;
|
||||
|
||||
const value = (e.target as HTMLTextAreaElement).value || "";
|
||||
@@ -124,7 +123,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOpenAutocomplete(!!AutocompleteEl);
|
||||
setOpenAutocomplete(!!AutocompleteEl && autocompleteQuick);
|
||||
}, [autocompleteQuick]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -137,12 +137,7 @@ const StepConfigurator: FC = () => {
|
||||
startIcon={<TimelineIcon/>}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
<p>
|
||||
STEP
|
||||
<p className="vm-step-control__value">
|
||||
{customStep}
|
||||
</p>
|
||||
</p>
|
||||
STEP {customStep}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
&-popper {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user