Compare commits

..

1 Commits

Author SHA1 Message Date
Haley Wang
1cc1d45503 vmalert-dashboard: fix stats panels visualization 2025-01-08 22:24:20 +08:00
1865 changed files with 439011 additions and 49170 deletions

View File

@@ -85,7 +85,7 @@ jobs:
restore-keys: go-artifacts-${{ runner.os }}-${{ matrix.scenario }}-
- name: Run tests
run: GOGC=10 make ${{ matrix.scenario}}
run: make ${{ matrix.scenario}}
- name: Publish coverage
uses: codecov/codecov-action@v5

View File

@@ -175,7 +175,7 @@
END OF TERMS AND CONDITIONS
Copyright 2019-2025 VictoriaMetrics, Inc.
Copyright 2019-2024 VictoriaMetrics, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -513,19 +513,19 @@ check-all: fmt vet golangci-lint govulncheck
clean-checkers: remove-golangci-lint remove-govulncheck
test:
go test ./lib/... ./app/...
DISABLE_FSYNC_FOR_TESTING=1 go test ./lib/... ./app/...
test-race:
go test -race ./lib/... ./app/...
DISABLE_FSYNC_FOR_TESTING=1 go test -race ./lib/... ./app/...
test-pure:
CGO_ENABLED=0 go test ./lib/... ./app/...
DISABLE_FSYNC_FOR_TESTING=1 CGO_ENABLED=0 go test ./lib/... ./app/...
test-full:
go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
DISABLE_FSYNC_FOR_TESTING=1 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
test-full-386:
GOARCH=386 go test -coverprofile=coverage.txt -covermode=atomic ./lib/... ./app/...
DISABLE_FSYNC_FOR_TESTING=1 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.63.4
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
remove-golangci-lint:
rm -rf `which golangci-lint`

View File

@@ -42,10 +42,6 @@ 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() {
@@ -90,10 +86,6 @@ 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)

View File

@@ -199,8 +199,8 @@ func (lmp *logMessageProcessor) AddRow(timestamp int64, fields, streamFields []l
lmp.bytesIngestedTotal.Add(n)
if len(fields) > *MaxFieldsPerLine {
line := logstorage.MarshalFieldsToJSON(nil, fields)
logger.Warnf("dropping log line with %d fields; it exceeds -insert.maxFieldsPerLine=%d; %s", len(fields), *MaxFieldsPerLine, line)
rf := logstorage.RowFormatter(fields)
logger.Warnf("dropping log line with %d fields; it exceeds -insert.maxFieldsPerLine=%d; %s", len(fields), *MaxFieldsPerLine, rf)
rowsDroppedTotalTooManyFields.Inc()
return
}

View File

@@ -8,10 +8,8 @@ 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; "+
"see https://docs.victoriametrics.com/victorialogs/faq/#what-length-a-log-record-is-expected-to-have")
MaxLineSizeBytes = flagutil.NewBytes("insert.maxLineSizeBytes", 256*1024, "The maximum size of a single line, which can be read by /insert/* handlers")
// 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; "+
"see https://docs.victoriametrics.com/victorialogs/faq/#how-many-fields-a-single-log-entry-may-contain")
MaxFieldsPerLine = flag.Int("insert.maxFieldsPerLine", 1000, "The maximum number of log fields per line, which can be read by /insert/* handlers")
)

View File

@@ -15,8 +15,6 @@ 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
@@ -28,9 +26,6 @@ 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
@@ -56,27 +51,26 @@ func NewLineReader(name string, r io.Reader) *LineReader {
// Check for Err in this case.
func (lr *LineReader) NextLine() bool {
for {
if lr.bufOffset >= len(lr.buf) {
if len(lr.buf) == 0 {
if lr.err != nil || lr.eofReached {
return false
}
if !lr.readMoreData() {
return false
}
if lr.bufOffset >= len(lr.buf) && lr.eofReached {
if len(lr.buf) == 0 && lr.eofReached {
return false
}
}
buf := lr.buf[lr.bufOffset:]
if n := bytes.IndexByte(buf, '\n'); n >= 0 {
lr.Line = buf[:n]
lr.bufOffset += n + 1
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:]...)
return true
}
if lr.eofReached {
lr.Line = buf
lr.bufOffset += len(buf)
lr.Line = append(lr.Line[:0], lr.buf...)
lr.buf = lr.buf[:0]
return true
}
if !lr.readMoreData() {
@@ -94,11 +88,6 @@ 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)

View File

@@ -2,19 +2,20 @@ package insertutils
import (
"fmt"
"math"
"strconv"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
)
// ExtractTimestampFromFields extracts timestamp in nanoseconds from the field with the name timeField at fields.
// ExtractTimestampRFC3339NanoFromFields extracts RFC3339 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 ExtractTimestampFromFields(timeField string, fields []logstorage.Field) (int64, error) {
func ExtractTimestampRFC3339NanoFromFields(timeField string, fields []logstorage.Field) (int64, error) {
for i := range fields {
f := &fields[i]
if f.Name != timeField {
@@ -47,24 +48,22 @@ func parseTimestamp(s string) (int64, error) {
return nsecs, nil
}
// ParseUnixTimestamp parses s as unix timestamp in seconds, milliseconds, microseconds or nanoseconds and returns the parsed timestamp in nanoseconds.
// ParseUnixTimestamp parses s as unix timestamp in either seconds or milliseconds and returns the parsed timestamp in nanoseconds.
func ParseUnixTimestamp(s string) (int64, error) {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("cannot parse unix timestamp from %q: %w", s, err)
}
if n < (1<<31) && n >= (-1<<31) {
// The timestamp is in seconds.
return n * 1e9, nil
// The timestamp is in seconds. Convert it to milliseconds
n *= 1e3
}
if n < 1e3*(1<<31) && n >= 1e3*(-1<<31) {
// The timestamp is in milliseconds.
return n * 1e6, 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 < 1e6*(1<<31) && n >= 1e6*(-1<<31) {
// The timestamp is in microseconds.
return n * 1e3, 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)
}
// The timestamp is in nanoseconds
n *= 1e6
return n, nil
}

View File

@@ -6,11 +6,11 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
)
func TestExtractTimestampFromFields_Success(t *testing.T) {
func TestExtractTimestampRFC3339NanoFromFields_Success(t *testing.T) {
f := func(timeField string, fields []logstorage.Field, nsecsExpected int64) {
t.Helper()
nsecs, err := ExtractTimestampFromFields(timeField, fields)
nsecs, err := ExtractTimestampRFC3339NanoFromFields(timeField, fields)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
@@ -51,18 +51,6 @@ func TestExtractTimestampFromFields_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"},
@@ -76,14 +64,14 @@ func TestExtractTimestampFromFields_Success(t *testing.T) {
}, 1718773640000000000)
}
func TestExtractTimestampFromFields_Error(t *testing.T) {
func TestExtractTimestampRFC3339NanoFromFields_Error(t *testing.T) {
f := func(s string) {
t.Helper()
fields := []logstorage.Field{
{Name: "time", Value: s},
}
nsecs, err := ExtractTimestampFromFields("time", fields)
nsecs, err := ExtractTimestampRFC3339NanoFromFields("time", fields)
if err == nil {
t.Fatalf("expecting non-nil error")
}
@@ -92,7 +80,6 @@ func TestExtractTimestampFromFields_Error(t *testing.T) {
}
}
// invalid time
f("foobar")
// incomplete time

View File

@@ -51,13 +51,20 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
lmp := cp.NewLogMessageProcessor("jsonline")
streamName := fmt.Sprintf("remoteAddr=%s, requestURI=%q", httpserver.GetQuotedRemoteAddr(r), r.RequestURI)
processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
err = processStreamInternal(streamName, reader, cp.TimeField, cp.MsgFields, lmp)
lmp.MustClose()
requestDuration.UpdateDuration(startTime)
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)
}
}
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) {
func processStreamInternal(streamName string, r io.Reader, timeField string, msgFields []string, lmp insertutils.LogMessageProcessor) error {
wcr := writeconcurrencylimiter.GetReader(r)
defer writeconcurrencylimiter.PutReader(wcr)
@@ -69,10 +76,10 @@ func processStreamInternal(streamName string, r io.Reader, timeField string, msg
wcr.DecConcurrency()
if err != nil {
errorsTotal.Inc()
logger.Warnf("jsonline: cannot read line #%d in /jsonline request: %s", n, err)
return fmt.Errorf("cannot read line #%d in /jsonline request: %s", n, err)
}
if !ok {
return
return nil
}
n++
}
@@ -89,17 +96,16 @@ func readLine(lr *insertutils.LineReader, timeField string, msgFields []string,
}
p := logstorage.GetJSONParser()
defer logstorage.PutJSONParser(p)
if err := p.ParseLogMessage(line); err != nil {
return true, fmt.Errorf("cannot parse json-encoded line: %w; line contents: %q", err, line)
return false, fmt.Errorf("cannot parse json-encoded log entry: %w", err)
}
ts, err := insertutils.ExtractTimestampFromFields(timeField, p.Fields)
ts, err := insertutils.ExtractTimestampRFC3339NanoFromFields(timeField, p.Fields)
if err != nil {
return true, fmt.Errorf("cannot get timestamp from json-encoded line: %w; line contents: %q", err, line)
return false, fmt.Errorf("cannot get timestamp: %w", err)
}
logstorage.RenameField(p.Fields, msgFields, "_msg")
lmp.AddRow(ts, p.Fields, nil)
logstorage.PutJSONParser(p)
return true, nil
}

View File

@@ -7,14 +7,16 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vlinsert/insertutils"
)
func TestProcessStreamInternal(t *testing.T) {
func TestProcessStreamInternal_Success(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)
processStreamInternal("test", r, timeField, msgFields, tlp)
if err := processStreamInternal("test", r, timeField, msgFields, tlp); err != nil {
t.Fatalf("unexpected error: %s", err)
}
if err := tlp.Verify(timestampsExpected, resultExpected); err != nil {
t.Fatal(err)
@@ -43,37 +45,22 @@ func TestProcessStreamInternal(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
data = "foobar"
timeField = "@timestamp"
msgField = "aaa"
timestampsExpected = nil
resultExpected = ``
f(data, timeField, msgField, timestampsExpected, resultExpected)
f("foobar")
// invalid timestamp field
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)
f(`{"time":"foobar"}`)
}

View File

@@ -560,7 +560,7 @@ func processLine(line []byte, currentYear int, timezone *time.Location, useLocal
if useLocalTimestamp {
ts = time.Now().UnixNano()
} else {
nsecs, err := insertutils.ExtractTimestampFromFields("timestamp", p.Fields)
nsecs, err := insertutils.ExtractTimestampRFC3339NanoFromFields("timestamp", p.Fields)
if err != nil {
return fmt.Errorf("cannot get timestamp from syslog line %q: %w", line, err)
}

View File

@@ -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)

View File

@@ -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 mode
\c - compact output
\logfmt - logfmt output mode
\wrap_long_lines - toggles wrapping long lines
\tail <query> - live tail <query> results

View File

@@ -45,8 +45,6 @@ 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; "+
@@ -256,9 +254,6 @@ 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)
}

View File

@@ -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" {

View File

@@ -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",baz="z"} (foo:bar or foo:baz) error _time:5m`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
}
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",baz="z"} (foo:bar or foo:baz) error _time:5m`)
f(`foo:(bar or baz) error _time:5m {"foo"=bar,baz="z"}`, `(foo:bar or foo:baz) error _time:5m {foo="bar",baz="z"}`)
}
func TestParseExtraStreamFilters_Failure(t *testing.T) {

View File

@@ -1,12 +1,13 @@
{
"files": {
"main.css": "./static/css/main.02a1c6cb.css",
"main.js": "./static/js/main.55c8060b.js",
"main.css": "./static/css/main.fa83344e.css",
"main.js": "./static/js/main.8ad2bc1f.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.02a1c6cb.css",
"static/js/main.55c8060b.js"
"static/css/main.fa83344e.css",
"static/js/main.8ad2bc1f.js"
]
}

View File

@@ -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.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>
<!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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -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(oldGroup *rule.Group, newGroup *rule.Group) {
oldGroup.UpdateWith(newGroup)
go func(old *rule.Group, new *rule.Group) {
old.UpdateWith(new)
wg.Done()
}(item.old, item.new)
}

View File

@@ -13,7 +13,6 @@ 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"
)
@@ -70,17 +69,7 @@ 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{}
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)
writeamRequest(b, alerts, am.argFunc, am.relabelConfigs)
req, err := http.NewRequest(http.MethodPost, am.addr.String(), b)
if err != nil {

View File

@@ -1,14 +1,15 @@
{% import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
) %}
{% stripspace %}
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) %}
{% func amRequest(alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) %}
[
{% for i, alert := range alerts %}
{% code lbls := lblss[i] %}
{% code lbls := alert.applyRelabelingIfNeeded(relabelCfg) %}
{% if len(lbls) == 0 %} {% continue %} {% endif %}
{
"startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %},
"generatorURL": {%q= generatorURL(alert) %},

View File

@@ -8,7 +8,7 @@ package notifier
import (
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
//line app/vmalert/notifier/alertmanager_request.qtpl:8
@@ -25,116 +25,122 @@ var (
)
//line app/vmalert/notifier/alertmanager_request.qtpl:8
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) {
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string, relabelCfg *promrelabel.ParsedConfigs) {
//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 := lblss[i]
lbls := alert.applyRelabelingIfNeeded(relabelCfg)
//line app/vmalert/notifier/alertmanager_request.qtpl:11
qw422016.N().S(`{"startsAt":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:13
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
//line app/vmalert/notifier/alertmanager_request.qtpl:13
qw422016.N().S(`,"generatorURL":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().Q(generatorURL(alert))
//line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:15
if !alert.End.IsZero() {
//line app/vmalert/notifier/alertmanager_request.qtpl:15
qw422016.N().S(`"endsAt":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:16
qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
//line app/vmalert/notifier/alertmanager_request.qtpl:16
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:17
//line app/vmalert/notifier/alertmanager_request.qtpl:12
if len(lbls) == 0 {
//line app/vmalert/notifier/alertmanager_request.qtpl:12
continue
//line app/vmalert/notifier/alertmanager_request.qtpl:12
}
//line app/vmalert/notifier/alertmanager_request.qtpl:12
qw422016.N().S(`{"startsAt":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
//line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().S(`,"generatorURL":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:15
qw422016.N().Q(generatorURL(alert))
//line app/vmalert/notifier/alertmanager_request.qtpl:15
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:16
if !alert.End.IsZero() {
//line app/vmalert/notifier/alertmanager_request.qtpl:16
qw422016.N().S(`"endsAt":`)
//line app/vmalert/notifier/alertmanager_request.qtpl:17
qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
//line app/vmalert/notifier/alertmanager_request.qtpl:17
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:18
}
//line app/vmalert/notifier/alertmanager_request.qtpl:18
qw422016.N().S(`"labels": {`)
//line app/vmalert/notifier/alertmanager_request.qtpl:19
//line app/vmalert/notifier/alertmanager_request.qtpl:20
ll := len(lbls)
//line app/vmalert/notifier/alertmanager_request.qtpl:20
//line app/vmalert/notifier/alertmanager_request.qtpl:21
for idx, l := range lbls {
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:22
qw422016.N().Q(l.Name)
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:22
qw422016.N().S(`:`)
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:22
qw422016.N().Q(l.Value)
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:22
if idx != ll-1 {
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:22
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:21
//line app/vmalert/notifier/alertmanager_request.qtpl:22
}
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:23
}
//line app/vmalert/notifier/alertmanager_request.qtpl:22
//line app/vmalert/notifier/alertmanager_request.qtpl:23
qw422016.N().S(`},"annotations": {`)
//line app/vmalert/notifier/alertmanager_request.qtpl:25
//line app/vmalert/notifier/alertmanager_request.qtpl:26
c := len(alert.Annotations)
//line app/vmalert/notifier/alertmanager_request.qtpl:26
for k, v := range alert.Annotations {
//line app/vmalert/notifier/alertmanager_request.qtpl:27
for k, v := range alert.Annotations {
//line app/vmalert/notifier/alertmanager_request.qtpl:28
c = c - 1
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:29
qw422016.N().Q(k)
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:29
qw422016.N().S(`:`)
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:29
qw422016.N().Q(v)
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:29
if c > 0 {
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:29
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:28
//line app/vmalert/notifier/alertmanager_request.qtpl:29
}
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:30
}
//line app/vmalert/notifier/alertmanager_request.qtpl:29
//line app/vmalert/notifier/alertmanager_request.qtpl:30
qw422016.N().S(`}}`)
//line app/vmalert/notifier/alertmanager_request.qtpl:32
//line app/vmalert/notifier/alertmanager_request.qtpl:33
if i != len(alerts)-1 {
//line app/vmalert/notifier/alertmanager_request.qtpl:32
//line app/vmalert/notifier/alertmanager_request.qtpl:33
qw422016.N().S(`,`)
//line app/vmalert/notifier/alertmanager_request.qtpl:32
//line app/vmalert/notifier/alertmanager_request.qtpl:33
}
//line app/vmalert/notifier/alertmanager_request.qtpl:33
//line app/vmalert/notifier/alertmanager_request.qtpl:34
}
//line app/vmalert/notifier/alertmanager_request.qtpl:33
//line app/vmalert/notifier/alertmanager_request.qtpl:34
qw422016.N().S(`]`)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
}
//line app/vmalert/notifier/alertmanager_request.qtpl:35
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) {
//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
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
streamamRequest(qw422016, alerts, generatorURL, lblss)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
streamamRequest(qw422016, alerts, generatorURL, relabelCfg)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
qt422016.ReleaseWriter(qw422016)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
}
//line app/vmalert/notifier/alertmanager_request.qtpl:35
func amRequest(alerts []Alert, generatorURL func(Alert) string, lblss [][]prompbmarshal.Label) string {
//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
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmalert/notifier/alertmanager_request.qtpl:35
writeamRequest(qb422016, alerts, generatorURL, lblss)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
writeamRequest(qb422016, alerts, generatorURL, relabelCfg)
//line app/vmalert/notifier/alertmanager_request.qtpl:36
qs422016 := string(qb422016.B)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
return qs422016
//line app/vmalert/notifier/alertmanager_request.qtpl:35
//line app/vmalert/notifier/alertmanager_request.qtpl:36
}

View File

@@ -105,16 +105,6 @@ 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)
@@ -178,20 +168,7 @@ func TestAlertManager_Send(t *testing.T) {
t.Fatalf("unexpected error %s", err)
}
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)
if c != 3 {
t.Fatalf("expected 3 calls(count from zero) to server got %d", c)
}
}

View File

@@ -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. Default value depends on the number of available CPU cores.")
concurrency = flag.Int("remoteWrite.concurrency", defaultConcurrency, "Defines number of writers for concurrent writing into remote write endpoint")
flushInterval = flag.Duration("remoteWrite.flushInterval", defaultFlushInterval, "Defines interval of flushes to remote write endpoint")
tlsInsecureSkipVerify = flag.Bool("remoteWrite.tlsInsecureSkipVerify", false, "Whether to skip tls verification when connecting to -remoteWrite.url")

View File

@@ -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(newGroup *Group) {
g.updateCh <- newGroup
func (g *Group) UpdateWith(new *Group) {
g.updateCh <- new
}
// DeepCopy returns a deep copy of group

View File

@@ -31,11 +31,7 @@ import (
)
var (
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.")
httpListenAddrs = flagutil.NewArrayString("httpListenAddr", "TCP address to listen for incoming http requests. See also -tls and -httpListenAddr.useProxyProtocol")
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")
@@ -95,21 +91,7 @@ func main() {
logger.Infof("starting vmauth at %q...", listenAddrs)
startTime := time.Now()
initAuthConfig()
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)
}
go httpserver.Serve(listenAddrs, useProxyProtocol, requestHandler)
logger.Infof("started vmauth in %.3f seconds", time.Since(startTime).Seconds())
pushmetrics.Init()
@@ -127,7 +109,7 @@ func main() {
logger.Infof("successfully stopped vmauth in %.3f seconds", time.Since(startTime).Seconds())
}
func internalRequestHandler(w http.ResponseWriter, r *http.Request) bool {
func requestHandler(w http.ResponseWriter, r *http.Request) bool {
switch r.URL.Path {
case "/-/reload":
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
@@ -138,17 +120,6 @@ func internalRequestHandler(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 {
@@ -251,7 +222,8 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo) {
isDefault = true
}
rtb := newReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
rtb := getReadTrackingBody(r.Body, maxRequestBodySizeToRetry.IntN())
defer putReadTrackingBody(rtb)
r.Body = rtb
maxAttempts := up.getBackendsCount()
@@ -587,11 +559,22 @@ type readTrackingBody struct {
bufComplete bool
}
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{}
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)
if maxBodySize < 0 {
maxBodySize = 0
}
@@ -614,6 +597,13 @@ 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 {

View File

@@ -52,7 +52,7 @@ func TestRequestHandler(t *testing.T) {
r.Header.Set("Pass-Header", "abc")
w := &fakeResponseWriter{}
if !requestHandlerWithInternalRoutes(w, r) {
if !requestHandler(w, r) {
t.Fatalf("unexpected false is returned from requestHandler")
}
@@ -195,7 +195,7 @@ unauthorized_user:
}
responseExpected = `
statusCode=401
Expected to receive non-empty authKey when -reloadAuthKey is set`
The provided authKey doesn't match -reloadAuthKey`
f(cfgStr, requestURL, backendHandler, responseExpected)
if err := reloadAuthKey.Set(origAuthKey); err != nil {
t.Fatalf("unexpected error: %s", err)
@@ -545,7 +545,8 @@ func TestReadTrackingBody_RetrySuccess(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")
@@ -580,7 +581,8 @@ func TestReadTrackingBody_RetrySuccessPartialRead(t *testing.T) {
t.Helper()
// Check the case with partial read
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
for i := 0; i < len(s); i++ {
buf := make([]byte, i)
@@ -629,7 +631,8 @@ func TestReadTrackingBody_RetryFailureTooBigBody(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")
@@ -678,7 +681,8 @@ func TestReadTrackingBody_RetryFailureZeroOrNegativeMaxBodySize(t *testing.T) {
f := func(s string, maxBodySize int) {
t.Helper()
rtb := newReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
rtb := getReadTrackingBody(io.NopCloser(bytes.NewBufferString(s)), maxBodySize)
defer putReadTrackingBody(rtb)
if !rtb.canRetry() {
t.Fatalf("canRetry() must return true before reading anything")

View File

@@ -596,8 +596,7 @@ 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. \n" +
"Rate limit is applied per worker, see `--vm-concurrency`.",
"By default, the rate limit is disabled. It can be useful for limiting load on source or destination databases.",
},
&cli.BoolFlag{
Name: vmInterCluster,

View File

@@ -40,15 +40,15 @@ type filter struct {
labelValue string
}
func (f filter) inRange(minV, maxV int64) bool {
func (f filter) inRange(min, max int64) bool {
fmin, fmax := f.min, f.max
if minV == 0 {
fmin = minV
if min == 0 {
fmin = min
}
if fmax == 0 {
fmax = maxV
fmax = max
}
return minV <= fmax && fmin <= maxV
return min <= fmax && fmin <= max
}
// 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}
minTime, maxTime, err := parseTime(cfg.Filter.TimeMin, cfg.Filter.TimeMax)
min, max, 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: minTime,
max: maxTime,
min: min,
max: max,
label: cfg.Filter.Label,
labelValue: cfg.Filter.LabelValue,
}

View File

@@ -98,13 +98,13 @@ func aggrMin(values []float64) float64 {
if pos < 0 {
return nan
}
minV := values[pos]
min := values[pos]
for _, v := range values[pos+1:] {
if !math.IsNaN(v) && v < minV {
minV = v
if !math.IsNaN(v) && v < min {
min = v
}
}
return minV
return min
}
func aggrMax(values []float64) float64 {
@@ -112,13 +112,13 @@ func aggrMax(values []float64) float64 {
if pos < 0 {
return nan
}
maxV := values[pos]
max := values[pos]
for _, v := range values[pos+1:] {
if !math.IsNaN(v) && v > maxV {
maxV = v
if !math.IsNaN(v) && v > max {
max = v
}
}
return maxV
return max
}
func aggrDiff(values []float64) float64 {
@@ -177,12 +177,12 @@ func aggrCount(values []float64) float64 {
}
func aggrRange(values []float64) float64 {
minV := aggrMin(values)
if math.IsNaN(minV) {
min := aggrMin(values)
if math.IsNaN(min) {
return nan
}
maxV := aggrMax(values)
return maxV - minV
max := aggrMax(values)
return max - min
}
func aggrMultiply(values []float64) float64 {

View File

@@ -2594,17 +2594,17 @@ func transformMinMax(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
}
f := nextSeriesConcurrentWrapper(nextSeries, func(s *series) (*series, error) {
values := s.Values
minV := aggrMin(values)
if math.IsNaN(minV) {
minV = 0
min := aggrMin(values)
if math.IsNaN(min) {
min = 0
}
maxV := aggrMax(values)
if math.IsNaN(maxV) {
maxV = 0
max := aggrMax(values)
if math.IsNaN(max) {
max = 0
}
vRange := maxV - minV
vRange := max - min
for i, v := range values {
v = (v - minV) / vRange
v = (v - min) / 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
maxV := aggrFunc(values)
max := aggrFunc(values)
for i, v := range values {
if v > maxV {
if v > max {
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
minV := aggrFunc(values)
min := aggrFunc(values)
for i, v := range values {
if v < minV {
if v < min {
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
minV := aggrMin(values)
min := aggrMin(values)
for i, v := range values {
values[i] = v - minV
values[i] = v - min
}
s.Tags["offsetToZero"] = fmt.Sprintf("%g", minV)
s.Tags["offsetToZero"] = fmt.Sprintf("%g", min)
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(currV, prevV, maxV, minV float64) (float64, float64) {
if !math.IsNaN(maxV) && currV > maxV {
func nonNegativeDelta(curr, prev, max, min float64) (float64, float64) {
if !math.IsNaN(max) && curr > max {
return nan, nan
}
if !math.IsNaN(minV) && currV < minV {
if !math.IsNaN(min) && curr < min {
return nan, nan
}
if math.IsNaN(currV) || math.IsNaN(prevV) {
return nan, currV
if math.IsNaN(curr) || math.IsNaN(prev) {
return nan, curr
}
if currV >= prevV {
return currV - prevV, currV
if curr >= prev {
return curr - prev, curr
}
if !math.IsNaN(maxV) {
if math.IsNaN(minV) {
minV = float64(0)
if !math.IsNaN(max) {
if math.IsNaN(min) {
min = float64(0)
}
return maxV + 1 + currV - prevV - minV, currV
return max + 1 + curr - prev - min, curr
}
if !math.IsNaN(minV) {
return currV - minV, currV
if !math.IsNaN(min) {
return curr - min, curr
}
return nan, currV
return nan, curr
}
// 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) {
maxV := aggrMax(s.Values)
if math.IsNaN(maxV) || maxV <= 0 {
max := aggrMax(s.Values)
if math.IsNaN(max) || max <= 0 {
return nil, nil
}
return s, nil

View File

@@ -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 could be passed via authKey query arg. It overrides -httpAuth.*")
deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey for metrics' deletion via /api/v1/admin/tsdb/delete_series and /tags/delSeries. 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 could be passed via authKey query arg. It overrides -httpAuth.*")
resetCacheAuthKey = flagutil.NewPassword("search.resetCacheAuthKey", "Optional authKey for resetting rollup cache via /internal/resetRollupResultCache call. 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")

View File

@@ -8,6 +8,7 @@ import (
"sort"
"sync"
"sync/atomic"
"time"
"unsafe"
"github.com/VictoriaMetrics/metrics"
@@ -1001,7 +1002,9 @@ 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)
@@ -1139,7 +1142,9 @@ 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
}
@@ -1291,6 +1296,8 @@ 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

View File

@@ -29,7 +29,6 @@ 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"
)
@@ -143,13 +142,10 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
WriteFederate(bb, rs)
return sw.maybeFlushBuffer(bb)
})
if err == nil {
err = sw.flush()
}
if err != nil && !netutil.IsTrivialNetworkError(err) {
if err != nil {
return fmt.Errorf("error during sending data to remote client: %w", err)
}
return nil
return sw.flush()
}
var federateDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/federate"}`)
@@ -230,13 +226,10 @@ func ExportCSVHandler(startTime time.Time, w http.ResponseWriter, r *http.Reques
}()
}
err = <-doneCh
if err == nil {
err = sw.flush()
}
if err != nil && !netutil.IsTrivialNetworkError(err) {
if err != nil {
return fmt.Errorf("error during sending the exported csv data to remote client: %w", err)
}
return nil
return sw.flush()
}
var exportCSVDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export/csv"}`)
@@ -288,13 +281,10 @@ func ExportNativeHandler(startTime time.Time, w http.ResponseWriter, r *http.Req
bb.B = dst
return sw.maybeFlushBuffer(bb)
})
if err == nil {
err = sw.flush()
}
if err != nil && !netutil.IsTrivialNetworkError(err) {
if err != nil {
return fmt.Errorf("error during sending native data to remote client: %w", err)
}
return nil
return sw.flush()
}
var exportNativeDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/export/native"}`)
@@ -451,19 +441,16 @@ func exportHandler(qt *querytracer.Tracer, w http.ResponseWriter, cp *commonPara
}()
}
err := <-doneCh
if err == nil {
err = sw.flush()
}
if err == nil {
if format == "promapi" {
WriteExportPromAPIFooter(bw, qt)
}
err = bw.Flush()
}
if err != nil && !netutil.IsTrivialNetworkError(err) {
if err != nil {
return fmt.Errorf("cannot send data to remote client: %w", err)
}
return nil
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()
}
type exportBlock struct {
@@ -494,8 +481,6 @@ 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)
}

View File

@@ -295,13 +295,13 @@ func aggrFuncMin(tss []*timeseries) []*timeseries {
}
dst := tss[0]
for i := range dst.Values {
minV := dst.Values[i]
min := dst.Values[i]
for _, ts := range tss {
if math.IsNaN(minV) || ts.Values[i] < minV {
minV = ts.Values[i]
if math.IsNaN(min) || ts.Values[i] < min {
min = ts.Values[i]
}
}
dst.Values[i] = minV
dst.Values[i] = min
}
return tss[:1]
}
@@ -313,13 +313,13 @@ func aggrFuncMax(tss []*timeseries) []*timeseries {
}
dst := tss[0]
for i := range dst.Values {
maxV := dst.Values[i]
max := dst.Values[i]
for _, ts := range tss {
if math.IsNaN(maxV) || ts.Values[i] > maxV {
maxV = ts.Values[i]
if math.IsNaN(max) || ts.Values[i] > max {
max = ts.Values[i]
}
}
dst.Values[i] = maxV
dst.Values[i] = max
}
return tss[:1]
}
@@ -793,7 +793,7 @@ func fillNaNsAtIdx(idx int, k float64, tss []*timeseries) {
}
}
func getIntK(k float64, maxV int) int {
func getIntK(k float64, max int) int {
if math.IsNaN(k) {
return 0
}
@@ -801,38 +801,38 @@ func getIntK(k float64, maxV int) int {
if kn < 0 {
return 0
}
if kn > maxV {
return maxV
if kn > max {
return max
}
return kn
}
func minValue(values []float64) float64 {
minV := nan
for len(values) > 0 && math.IsNaN(minV) {
minV = values[0]
min := nan
for len(values) > 0 && math.IsNaN(min) {
min = values[0]
values = values[1:]
}
for _, v := range values {
if !math.IsNaN(v) && v < minV {
minV = v
if !math.IsNaN(v) && v < min {
min = v
}
}
return minV
return min
}
func maxValue(values []float64) float64 {
maxV := nan
for len(values) > 0 && math.IsNaN(maxV) {
maxV = values[0]
max := nan
for len(values) > 0 && math.IsNaN(max) {
max = values[0]
values = values[1:]
}
for _, v := range values {
if !math.IsNaN(v) && v > maxV {
maxV = v
if !math.IsNaN(v) && v > max {
max = v
}
}
return maxV
return max
}
func avgValue(values []float64) float64 {

View File

@@ -483,11 +483,8 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
mLeft, mRight := createTimeseriesMapByTagSet(bfa.be, bfa.left, bfa.right)
var rvs []*timeseries
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...)
for _, tss := range mLeft {
rvs = append(rvs, tss...)
}
// Sort left-hand-side series by metric name as Prometheus does.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5393
@@ -500,10 +497,7 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
rvs = append(rvs, tssRight...)
continue
}
fillLeftNaNsWithRightValuesOrMerge(tssLeft, tssRight)
// tssRight might be filled with NaNs after merge
tssRight = removeEmptySeries(tssRight)
rvs = append(rvs, tssRight...)
fillLeftNaNsWithRightValues(tssLeft, tssRight)
}
// Sort the added right-hand-side series by metric name as Prometheus does.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5393
@@ -532,35 +526,6 @@ 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

View File

@@ -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,
union(label_set(90, "foo", "bar", "le", "10"),
label_set(NaN, "foo", "bar", "le", "30"),
label_set(300, "foo", "bar", "le", "+Inf"))
label_set(90, "foo", "bar", "le", "10")
or label_set(NaN, "foo", "bar", "le", "30")
or label_set(300, "foo", "bar", "le", "+Inf")
),0.01)`
r := netstorage.Result{
MetricName: metricNameExpected,
@@ -9409,384 +9409,7 @@ 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) {

View File

@@ -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, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
preFunc = func(values []float64, _ []int64) {
removeCounterResets(values)
}
}
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, timestamps []int64) {
removeCounterResets(values, timestamps, lookbackDelta)
preFunc = func(values []float64, _ []int64) {
removeCounterResets(values)
}
}
rf := rollupAggrFuncs[aggrFuncName]
@@ -520,8 +520,7 @@ type rollupFuncArg struct {
// Timestamps for values.
timestamps []int64
// Real value preceding values.
// Is populated if preceding value is within the -search.maxStalenessInterval (rc.LookbackDelta).
// Real value preceding values without restrictions on staleness interval.
realPrevValue float64
// Real value which goes after values.
@@ -765,18 +764,10 @@ 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 {
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
}
rfa.realPrevValue = values[i-1]
} else {
rfa.realPrevValue = nan
}
if j < len(values) {
rfa.realNextValue = values[j]
@@ -900,7 +891,7 @@ func getMaxPrevInterval(scrapeInterval int64) int64 {
return scrapeInterval + scrapeInterval/8
}
func removeCounterResets(values []float64, timestamps []int64, maxStalenessInterval int64) {
func removeCounterResets(values []float64) {
// There is no need in handling NaNs here, since they are impossible
// on values from vmstorage.
if len(values) == 0 {
@@ -919,16 +910,6 @@ func removeCounterResets(values []float64, timestamps []int64, maxStalenessInter
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,
@@ -1701,9 +1682,9 @@ func rollupRateOverSum(rfa *rollupFuncArg) float64 {
}
func rollupRange(rfa *rollupFuncArg) float64 {
maxV := rollupMax(rfa)
minV := rollupMin(rfa)
return maxV - minV
max := rollupMax(rfa)
min := rollupMin(rfa)
return max - min
}
func rollupSum2(rfa *rollupFuncArg) float64 {
@@ -2211,38 +2192,38 @@ func rollupClose(rfa *rollupFuncArg) float64 {
func rollupHigh(rfa *rollupFuncArg) float64 {
values := getCandlestickValues(rfa)
maxV := getFirstValueForCandlestick(rfa)
if math.IsNaN(maxV) {
max := getFirstValueForCandlestick(rfa)
if math.IsNaN(max) {
if len(values) == 0 {
return nan
}
maxV = values[0]
max = values[0]
values = values[1:]
}
for _, v := range values {
if v > maxV {
maxV = v
if v > max {
max = v
}
}
return maxV
return max
}
func rollupLow(rfa *rollupFuncArg) float64 {
values := getCandlestickValues(rfa)
minV := getFirstValueForCandlestick(rfa)
if math.IsNaN(minV) {
min := getFirstValueForCandlestick(rfa)
if math.IsNaN(min) {
if len(values) == 0 {
return nan
}
minV = values[0]
min = values[0]
values = values[1:]
}
for _, v := range values {
if v < minV {
minV = v
if v < min {
min = v
}
}
return minV
return min
}
func rollupModeOverTime(rfa *rollupFuncArg) float64 {

View File

@@ -5,8 +5,6 @@ import (
"testing"
"github.com/VictoriaMetrics/metricsql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
)
var (
@@ -117,49 +115,31 @@ func TestRollupIderivDuplicateTimestamps(t *testing.T) {
}
func TestRemoveCounterResets(t *testing.T) {
removeCounterResets(nil, nil, 0)
removeCounterResets(nil)
values := append([]float64{}, testValues...)
timestamps := append([]int64{}, testTimestamps...)
removeCounterResets(values, timestamps, 0)
removeCounterResets(values)
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}
timestampsExpected := []int64{0, 1, 2, 3}
removeCounterResets(values, timestampsExpected, 0)
removeCounterResets(values)
valuesExpected = []float64{-100, -100, -100, -100}
timestampsExpected := []int64{0, 1, 2, 3}
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}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
removeCounterResets(values, timestampsExpected, 0)
removeCounterResets(values)
valuesExpected = []float64{100, 100, 125, 125, 145, 195}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5}
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}
timestampsExpected = []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
removeCounterResets(values, timestampsExpected, 0)
removeCounterResets(values)
var prev float64
for i, v := range values {
if v < prev {
@@ -184,7 +164,7 @@ func TestDeltaValues(t *testing.T) {
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values, testTimestamps, 0)
removeCounterResets(values)
deltaValues(values)
valuesExpected = []float64{34, 10, 21, 33, 34, 65, 12, 32, 32, 2, 0, 0}
testRowsEqual(t, values, testTimestamps, valuesExpected, testTimestamps)
@@ -206,7 +186,7 @@ func TestDerivValues(t *testing.T) {
// remove counter resets
values = append([]float64{}, testValues...)
removeCounterResets(values, testTimestamps, 0)
removeCounterResets(values)
derivValues(values, testTimestamps)
valuesExpected = []float64{3400, 1111.111111111111, 1750, 2538.4615384615386, 3090.909090909091, 3611.1111111111113,
6000, 1882.3529411764705, 1777.7777777777778, 400, 0, 0}
@@ -237,7 +217,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, rfa.timestamps, 0)
removeCounterResets(rfa.values)
}
for i := 0; i < 5; i++ {
v := rf(&rfa)
@@ -1607,229 +1587,3 @@ 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)
})
}

View File

@@ -15,7 +15,6 @@ 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. "+
@@ -59,12 +58,6 @@ 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 {

View File

@@ -99,8 +99,7 @@ func TestParseMetricSelectorSuccess(t *testing.T) {
f(`{foo="bar"}`)
f(`{:f:oo=~"bar.+"}`)
f(`foo {bar != "baz"}`)
f(` { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
f(` { bar !~ "^ddd(x+)$", a="ss", "foo"} `)
f(` foo { bar !~ "^ddd(x+)$", a="ss", __name__="sffd"} `)
f(`(foo)`)
f(`\п\р\и\в\е{\ы="111"}`)
}

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "./static/css/main.7fa18e1b.css",
"main.js": "./static/js/main.ba08300f.js",
"main.css": "./static/css/main.876c56b7.css",
"main.js": "./static/js/main.caf36c39.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.7fa18e1b.css",
"static/js/main.ba08300f.js"
"static/css/main.876c56b7.css",
"static/js/main.caf36c39.js"
]
}

View File

@@ -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.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>
<!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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -67,8 +67,6 @@ 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")
)
@@ -102,7 +100,6 @@ 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)
@@ -584,7 +581,6 @@ 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()))
@@ -596,7 +592,6 @@ 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)
@@ -611,7 +606,6 @@ 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()))
@@ -622,7 +616,6 @@ 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())
@@ -633,7 +626,6 @@ 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())

View File

@@ -1,4 +1,4 @@
FROM golang:1.23.6 AS build-web-stage
FROM golang:1.23.4 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.2
FROM alpine:3.21.0
USER root
COPY --from=build-web-stage /build/web-amd64 /app/web

View File

@@ -1 +1 @@
VITE_APP_TYPE=victoriametrics
FAST_REFRESH=false

View File

@@ -1 +0,0 @@
VITE_APP_TYPE=victorialogs

View File

@@ -1 +0,0 @@
VITE_APP_TYPE=vmanomaly

View File

@@ -0,0 +1,48 @@
// 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"
}
]
}
};

View File

@@ -0,0 +1,42 @@
/* 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";
}
}
)
)
);

View File

@@ -1,23 +0,0 @@
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
};
}

View File

@@ -1,90 +0,0 @@
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,
},
}];

File diff suppressed because it is too large Load Diff

View File

@@ -3,40 +3,50 @@
"version": "0.1.0",
"private": true,
"homepage": "./",
"type": "module",
"dependencies": {
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.get": "^4.4.9",
"@types/qs": "^6.9.18",
"@types/react": "^19.0.8",
"@types/react-input-mask": "^3.0.6",
"@types/lodash.throttle": "^4.1.9",
"@types/node": "^22.5.4",
"@types/qs": "^6.9.15",
"@types/react-input-mask": "^3.0.5",
"@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",
"marked": "^15.0.6",
"marked-emoji": "^1.4.3",
"preact": "^10.25.4",
"qs": "^6.14.0",
"lodash.throttle": "^4.1.1",
"marked": "^14.1.2",
"marked-emoji": "^1.4.2",
"preact": "^10.23.2",
"qs": "^6.13.0",
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.1.5",
"uplot": "^1.6.31",
"vite": "^6.0.11",
"web-vitals": "^4.2.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"
},
"scripts": {
"prestart": "npm run copy-metricsql-docs",
"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"
"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"
]
},
"browserslist": {
"production": [
@@ -51,24 +61,26 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@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",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0",
"cross-env": "^7.0.3",
"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"
"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"
}
}
}

View File

@@ -38,4 +38,4 @@ const AppAnomaly: FC = () => {
</>;
};
export default AppAnomaly;
export default AppAnomaly;

View File

@@ -1,6 +1,3 @@
import uPlot from "uplot";
import { ReactNode } from "react";
export interface MetricBase {
group: number;
metric: {
@@ -9,13 +6,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 {
@@ -46,24 +43,10 @@ export interface Logs {
export interface LogHits {
timestamps: string[];
values: number[];
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;
total?: number;
fields: {
[key: string]: string;
};
}
export interface ReportMetaData {

View File

@@ -1,23 +1,22 @@
import React, { FC, useCallback, useMemo, useRef, useState } from "preact/compat";
import React, { FC, 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, { getLabelFromLogHit } from "./hooks/useBarHitsOptions";
import useBarHitsOptions 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 { LegendLogHits, LogHits } from "../../../api/types";
import { 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[];
@@ -58,29 +57,6 @@ 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);
@@ -145,7 +121,6 @@ const BarHitsChart: FC<Props> = ({ logHits, data: _data, period, setPeriod, onAp
<BarHitsLegend
uPlotInst={uPlotInst}
onApplyFilter={onApplyFilter}
legendDetails={legendDetails}
/>
)}
</div>

View File

@@ -1,53 +1,83 @@
import React, { FC, useEffect, useState } from "preact/compat";
import React, { FC, useCallback, useEffect, useState } from "preact/compat";
import uPlot, { Series } from "uplot";
import "./style.scss";
import "../../Line/Legend/style.scss";
import BarHitsLegendItem from "./BarHitsLegendItem";
import { LegendLogHits } from "../../../../api/types";
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";
interface Props {
uPlotInst: uPlot;
legendDetails: LegendLogHits[];
onApplyFilter: (value: string) => void;
}
const BarHitsLegend: FC<Props> = ({ uPlotInst, legendDetails, onApplyFilter }) => {
const BarHitsLegend: FC<Props> = ({ uPlotInst, onApplyFilter }) => {
const [series, setSeries] = useState<Series[]>([]);
const totalHits = legendDetails[0]?.totalHits || 0;
const [pairs, setPairs] = useState<string[][]>([]);
const getSeries = () => {
return uPlotInst.series.filter(s => s.scale !== "x");
};
const handleRedrawGraph = () => {
uPlotInst.redraw();
setSeries(getSeries());
};
useEffect(() => {
setSeries(getSeries());
const updateSeries = useCallback(() => {
const series = uPlotInst.series.filter(s => s.scale !== "x");
setSeries(series);
setPairs(series.map(s => getStreamPairs(s.label || "")));
}, [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">
{legendDetails.map((legend) => (
<BarHitsLegendItem
key={legend.label}
legend={legend}
series={series}
onRedrawGraph={handleRedrawGraph}
onApplyFilter={onApplyFilter}
/>
{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>
))}
<div className="vm-bar-hits-legend-info">
<div>
Total hits: <b>{totalHits.toLocaleString("en-US")}</b>
</div>
<div>
<code>L-Click</code> toggles visibility.&nbsp;
<code>R-Click</code> opens menu.
</div>
</div>
</div>
);
};

View File

@@ -1,92 +0,0 @@
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;

View File

@@ -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 {
max-width: 50%;
display: flex;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: $padding-small;
font-size: $font-size-small;
padding: $padding-small $padding-global;
font-size: 12px;
padding: 0 $padding-small;
border-radius: $border-radius-small;
cursor: pointer;
transition: 0.2s;
@@ -27,44 +27,34 @@
}
&__marker {
min-width: 14px;
max-width: 14px;
width: 14px;
height: 14px;
border: $color-background-block;
}
&__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-pairs {
display: flex;
gap: $padding-small;
&__total {
color: $color-text-secondary;
font-style: italic;
grid-column: 2;
&__value {
padding: $padding-small 0;
&:hover {
text-decoration: underline;
}
&:after {
content: ",";
}
&:last-child:after {
content: "";
}
}
}
}
&-info {
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;
}
list-style-position: inside;
}
}

View File

@@ -5,6 +5,7 @@ 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";
@@ -23,20 +24,27 @@ 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("true", "fill");
const [fill, setFill] = useStateSearchParams(false, "fill");
const [hideChart, setHideChart] = useStateSearchParams(false, "hide_chart");
const options: GraphOptions = useMemo(() => ({
graphStyle: GRAPH_STYLES.BAR,
graphStyle,
stacked,
fill: fill === "true",
fill,
hideChart,
}), [stacked, fill, hideChart]);
}), [graphStyle, stacked, fill, hideChart]);
const handleChangeGraphStyle = (val: string) => () => {
setGraphStyle(val as GRAPH_STYLES);
searchParams.set("graph", val);
setSearchParams(searchParams);
};
const handleChangeFill = (val: boolean) => {
setFill(`${val}`);
searchParams.set("fill", `${val}`);
setFill(val);
val ? searchParams.set("fill", "true") : searchParams.delete("fill");
setSearchParams(searchParams);
};
@@ -89,6 +97,21 @@ 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"}
@@ -99,7 +122,7 @@ const BarHitsOptions: FC<Props> = ({ onChange }) => {
<div className="vm-bar-hits-options-settings-item">
<Switch
label={"Fill"}
value={fill === "true"}
value={fill}
onChange={handleChangeFill}
/>
</div>

View File

@@ -11,12 +11,12 @@
&-settings {
display: grid;
align-items: flex-start;
min-width: 200px;
gap: $padding-global;
padding-bottom: $padding-global;
min-width: 200px;
&-item {
padding: 0 $padding-global;
border-bottom: $border-divider;
padding: 0 $padding-global $padding-global;
&_list {
padding: 0;

View File

@@ -5,7 +5,6 @@ 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;
@@ -27,7 +26,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;
const label = targetSeries?.label || "other";
const show = targetSeries?.show;
return {
label,
@@ -35,7 +34,7 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
value,
show
};
}).filter(item => item.value > 0 && item.show).sort(sortLogHits("value"));
}).filter(item => item.value > 0 && item.show).sort((a, b) => b.value - a.value);
const point = {
top: tooltipItems[0] ? uPlotInst?.valToPos?.(tooltipItems[0].value, "y") || 0 : 0,
@@ -105,24 +104,21 @@ const BarHitsTooltip: FC<Props> = ({ data, focusDataIdx, uPlotInst }) => {
className="vm-chart-tooltip-data__marker"
style={{ background: item.stroke }}
/>
<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>
{item.label}: <b>{item.value}</b>
</p>
</div>
))}
</div>
{tooltipData.values.length > 1 && (
<div className="vm-chart-tooltip-data">
<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>
Total records: <b>{tooltipData.total}</b>
</p>
</div>
)}
<div className="vm-chart-tooltip-header">
<div className="vm-chart-tooltip-header__title vm-bar-hits-tooltip__date">
<div className="vm-chart-tooltip-header__title">
{tooltipData.timestamp}
</div>
</div>

View File

@@ -9,23 +9,4 @@
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;
}
}

View File

@@ -1,50 +0,0 @@
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;

View File

@@ -1,64 +0,0 @@
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;

View File

@@ -1,74 +0,0 @@
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;

View File

@@ -1,116 +0,0 @@
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;

View File

@@ -1,23 +0,0 @@
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;

View File

@@ -1,178 +0,0 @@
@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;
}
}

View File

@@ -36,14 +36,6 @@ 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,
@@ -67,16 +59,16 @@ const useBarHitsOptions = ({
let colorN = 0;
return data.map((_d, i) => {
if (i === 0) return {}; // 0 index is xAxis(timestamps)
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++;
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++;
return {
label,
label: label || "other",
width: strokeWidth[graphOptions.graphStyle],
spanGaps: true,
stroke: color,
fill: graphOptions.fill ? color + (target?._isOther ? "" : "80") : "",
fill: graphOptions.fill ? color + "80" : "",
paths: getSeriesPaths(graphOptions.graphStyle),
};
});

View File

@@ -1,6 +1,7 @@
import React, { FC, useCallback, useEffect, useRef, useState, createPortal } from "preact/compat";
import React, { FC, useCallback, useEffect, useRef, useState } 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";
@@ -48,7 +49,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
onClose && onClose(id);
};
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement>) => {
const handleMouseDown = (e: ReactMouseEvent) => {
setMoved(true);
setMoving(true);
const { clientX, clientY } = e;
@@ -106,7 +107,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
if (!u) return null;
return createPortal((
return ReactDOM.createPortal((
<div
className={classNames({
"vm-chart-tooltip": true,

View File

@@ -32,11 +32,6 @@ $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;
@@ -95,8 +90,6 @@ $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);

View File

@@ -1,5 +1,4 @@
@use "src/styles/variables" as *;
@use 'sass:color';
$color-bar: #33BB55;
$color-bar-highest: #F79420;
@@ -8,7 +7,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 {
@@ -55,19 +54,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: color.scale($color-bar, $lightness: 40%);
background-color: lighten($color-bar, 10%);
}
&:first-child {
background-color: $color-bar-highest;
&:hover {
background-color: color.scale($color-bar-highest, $lightness: 40%);
background-color: lighten($color-bar-highest, 10%);
}
}
}

View File

@@ -12,11 +12,14 @@ 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;
}
@@ -45,21 +48,21 @@ const GlobalSettings: FC = () => {
const controls = [
{
show: !appModeEnable && !APP_TYPE_LOGS,
show: !appModeEnable && !isLogsApp,
component: <ServerConfigurator
ref={serverSettingRef}
onClose={handleClose}
/>
},
{
show: !APP_TYPE_LOGS,
show: !isLogsApp,
component: <LimitsConfigurator
ref={limitsSettingRef}
onClose={handleClose}
/>
},
{
show: APP_TYPE_LOGS,
show: isLogsApp,
component: <SwitchMarkdownParsing/>
},
{

View File

@@ -36,40 +36,35 @@ const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYax
"vm-axes-limits_mobile": isMobile
})}
>
<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}
/>
<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>
{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>;
};

View File

@@ -8,14 +8,10 @@ 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,
@@ -23,13 +19,11 @@ interface GraphSettingsProps {
value: boolean,
onChange: (value: boolean) => void,
},
isHistogram?: boolean,
}
const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, toggleEnableLimits, spanGaps }) => {
const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits, spanGaps }) => {
const popperRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLDivElement>(null);
const displayHistogramMode = isHistogramData(data);
const {
value: openPopper,
@@ -70,7 +64,6 @@ const GraphSettings: FC<GraphSettingsProps> = ({ data, yaxis, setYaxisLimits, to
spanGaps={spanGaps.value}
onChange={spanGaps.onChange}
/>
{displayHistogramMode && <GraphTypeSwitcher onChange={handleClose}/>}
</div>
</div>
</Popper>

View File

@@ -1,36 +0,0 @@
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;

View File

@@ -1,16 +0,0 @@
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 };
};

View File

@@ -10,17 +10,14 @@ interface Props {
const LinesConfigurator: FC<Props> = ({ spanGaps, onChange }) => {
const { isMobile } = useDeviceDetect();
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>
);
return <div>
<Switch
value={spanGaps}
onChange={onChange}
label="Connect null values"
fullWidth={isMobile}
/>
</div>;
};
export default LinesConfigurator;

View File

@@ -1,31 +1,15 @@
@use "src/styles/variables" as *;
.vm-graph-settings {
display: flex;
align-items: center;
gap: $padding-small;
&-popper {
display: grid;
gap: $padding-global;
padding: $padding-small $padding-large $padding-large;
min-width: 300px;
padding: 0 0 $padding-global;
&__body {
display: grid;
gap: $padding-large;
}
}
&-row {
display: grid;
gap: $padding-small;
grid-template-columns: minmax(150px, max-content) 1fr;
&__label {
&:after{
content: ":";
}
padding: 0 $padding-global;
}
}
}

View File

@@ -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;
firstKey && this.map.delete(firstKey);
this.map.delete(firstKey);
}
this.map.set(JSON.stringify(key), value);
}

View File

@@ -1,6 +1,7 @@
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { KeyboardEvent } from "react";
import { ErrorTypes } from "../../../types";
import TextField, { TextFieldKeyboardEvent } from "../../Main/TextField/TextField";
import TextField from "../../Main/TextField/TextField";
import "./style.scss";
import { QueryStats } from "../../../api/types";
import { partialWarning, seriesFetchedWarning } from "./warningText";
@@ -80,7 +81,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
setCaretPositionInput([caretPosition, caretPosition]);
};
const handleKeyDown = (e: TextFieldKeyboardEvent) => {
const handleKeyDown = (e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, shiftKey } = e;
const value = (e.target as HTMLTextAreaElement).value || "";
@@ -123,7 +124,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
};
useEffect(() => {
setOpenAutocomplete(!!AutocompleteEl && autocompleteQuick);
setOpenAutocomplete(!!AutocompleteEl);
}, [autocompleteQuick]);
useEffect(() => {

View File

@@ -137,7 +137,12 @@ const StepConfigurator: FC = () => {
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
STEP {customStep}
<p>
STEP
<p className="vm-step-control__value">
{customStep}
</p>
</p>
</Button>
</Tooltip>
)}

View File

@@ -8,6 +8,11 @@
text-transform: none;
}
&__value {
display: inline;
margin-left: 3px;
}
&-popper {
display: grid;
gap: $padding-small;

View File

@@ -1,244 +0,0 @@
import React, { FC, useMemo, useState } from "preact/compat";
import useBoolean from "../../../hooks/useBoolean";
import { RestartIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import Tooltip from "../../Main/Tooltip/Tooltip";
import { Logs } from "../../../api/types";
import Select from "../../Main/Select/Select";
import { useSearchParams } from "react-router-dom";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import TextField from "../../Main/TextField/TextField";
import dayjs from "dayjs";
import Hyperlink from "../../Main/Hyperlink/Hyperlink";
import {
LOGS_DISPLAY_FIELDS,
LOGS_GROUP_BY,
LOGS_DATE_FORMAT,
LOGS_URL_PARAMS,
WITHOUT_GROUPING
} from "../../../constants/logs";
const {
GROUP_BY,
NO_WRAP_LINES,
COMPACT_GROUP_HEADER,
DISPLAY_FIELDS,
DATE_FORMAT
} = LOGS_URL_PARAMS;
const title = "Group view settings";
interface Props {
logs: Logs[];
}
const GroupLogsConfigurators: FC<Props> = ({ logs }) => {
const [searchParams, setSearchParams] = useSearchParams();
const groupBy = searchParams.get(GROUP_BY) || LOGS_GROUP_BY;
const noWrapLines = searchParams.get(NO_WRAP_LINES) === "true";
const compactGroupHeader = searchParams.get(COMPACT_GROUP_HEADER) === "true";
const displayFieldsString = searchParams.get(DISPLAY_FIELDS) || "";
const displayFields = displayFieldsString ? displayFieldsString.split(",") : [LOGS_DISPLAY_FIELDS];
const [dateFormat, setDateFormat] = useState(searchParams.get(DATE_FORMAT) || LOGS_DATE_FORMAT);
const [errorFormat, setErrorFormat] = useState("");
const isGroupChanged = groupBy !== LOGS_GROUP_BY;
const isDisplayFieldsChanged = displayFields.length !== 1 || displayFields[0] !== LOGS_DISPLAY_FIELDS;
const isTimeChanged = searchParams.get(DATE_FORMAT) !== LOGS_DATE_FORMAT;
const hasChanges = [
isGroupChanged,
isDisplayFieldsChanged,
noWrapLines,
compactGroupHeader,
isTimeChanged
].some(Boolean);
const logsKeys = useMemo(() => {
return Array.from(new Set(logs.map(l => Object.keys(l)).flat()));
}, [logs]);
const {
value: openModal,
toggle: toggleOpen,
setFalse: handleClose,
} = useBoolean(false);
const handleSelectGroupBy = (key: string) => {
searchParams.set(GROUP_BY, key);
setSearchParams(searchParams);
};
const handleSelectDisplayField = (value: string) => {
const prev = displayFields;
const newDisplayFields = prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value];
searchParams.set(DISPLAY_FIELDS, newDisplayFields.join(","));
setSearchParams(searchParams);
};
const handleResetDisplayFields = () => {
searchParams.delete(DISPLAY_FIELDS);
setSearchParams(searchParams);
};
const toggleWrapLines = () => {
searchParams.set(NO_WRAP_LINES, String(!noWrapLines));
setSearchParams(searchParams);
};
const toggleCompactGroupHeader = () => {
searchParams.set(COMPACT_GROUP_HEADER, String(!compactGroupHeader));
setSearchParams(searchParams);
};
const handleChangeDateFormat = (format: string) => {
const date = new Date();
if (!dayjs(date, format, true).isValid()) {
setErrorFormat("Invalid date format");
}
setDateFormat(format);
};
const handleSaveAndClose = () => {
searchParams.set(DATE_FORMAT, dateFormat);
setSearchParams(searchParams);
handleClose();
};
const tooltipContent = () => {
if (!hasChanges) return title;
return (
<div className="vm-group-logs-configurator__tooltip">
<p>{title}</p>
<hr/>
<ul>
{isGroupChanged && <li>Group by <code>{`"${groupBy}"`}</code></li>}
{isDisplayFieldsChanged && <li>Display fields: {displayFields.length || 1}</li>}
{noWrapLines && <li>Single-line text is enabled</li>}
{compactGroupHeader && <li>Compact group header is enabled</li>}
{isTimeChanged && <li>Date format: <code>{dateFormat}</code></li>}
</ul>
</div>
);
};
return (
<>
<div className="vm-group-logs-configurator-button">
<Tooltip title={tooltipContent()}>
<Button
variant="text"
startIcon={<SettingsIcon/>}
onClick={toggleOpen}
ariaLabel={title}
/>
</Tooltip>
{hasChanges && <span className="vm-group-logs-configurator-button__marker"/>}
</div>
{openModal && (
<Modal
title={title}
onClose={handleSaveAndClose}
>
<div className="vm-group-logs-configurator">
<div className="vm-group-logs-configurator-item">
<Select
value={groupBy}
list={[WITHOUT_GROUPING, ...logsKeys]}
label="Group by field"
placeholder="Group by field"
onChange={handleSelectGroupBy}
searchable
/>
<Tooltip title={"Reset grouping"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={() => handleSelectGroupBy(LOGS_GROUP_BY)}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info">
Select a field to group logs by (default: <code>{LOGS_GROUP_BY}</code>).
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Select
value={displayFields}
list={logsKeys}
label="Display fields"
placeholder="Display fields"
onChange={handleSelectDisplayField}
searchable
/>
<Tooltip title={"Clear fields"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={handleResetDisplayFields}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info">
Select fields to display instead of the message (default: <code>{LOGS_DISPLAY_FIELDS}</code>).
</span>
</div>
<div className="vm-group-logs-configurator-item">
<TextField
autofocus
label="Date format"
value={dateFormat}
onChange={handleChangeDateFormat}
error={errorFormat}
/>
<Tooltip title={"Reset format"}>
<Button
variant="text"
color="primary"
startIcon={<RestartIcon/>}
onClick={() => setDateFormat(LOGS_DATE_FORMAT)}
/>
</Tooltip>
<span className="vm-group-logs-configurator-item__info vm-group-logs-configurator-item__info_input">
Set the date format (e.g., <code>YYYY-MM-DD HH:mm:ss</code>).
Learn more in <Hyperlink
href="https://day.js.org/docs/en/display/format"
>this documentation</Hyperlink>. <br/>
Your current date format: <code>{dayjs().format(dateFormat || LOGS_DATE_FORMAT)}</code>
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Switch
value={noWrapLines}
onChange={toggleWrapLines}
label="Single-line message"
/>
<span className="vm-group-logs-configurator-item__info">
Displays message in a single line and truncates it with an ellipsis if it exceeds the available space
</span>
</div>
<div className="vm-group-logs-configurator-item">
<Switch
value={compactGroupHeader}
onChange={toggleCompactGroupHeader}
label="Compact group header"
/>
<span className="vm-group-logs-configurator-item__info">
Shows group headers in one line with a &quot;+N more&quot; badge for extra fields.
</span>
</div>
</div>
</Modal>
)}
</>
);
};
export default GroupLogsConfigurators;

View File

@@ -1,48 +0,0 @@
@use "src/styles/variables" as *;
.vm-group-logs-configurator {
display: grid;
gap: calc($padding-large * 2);
padding: $padding-global 0;
width: 600px;
&-item {
display: grid;
grid-template-columns: 1fr 31px;
align-items: center;
justify-content: stretch;
gap: 0 $padding-small;
&__info {
margin-top: $padding-small;
grid-column: 1/span 2;
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
&_input {
margin-top: 0;
}
}
}
&-button {
position: relative;
&__marker {
position: absolute;
top: 6px;
left: 6px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: $color-secondary;
}
}
&__tooltip {
ul {
list-style-position: inside;
}
}
}

View File

@@ -30,10 +30,6 @@ const Accordion: FC<AccordionProps> = ({
onChange && onChange(isOpen);
}, [isOpen]);
useEffect(() => {
setIsOpen(defaultExpanded);
}, [defaultExpanded]);
return (
<>
<header

Some files were not shown because too many files have changed in this diff Show More