Compare commits

..

2 Commits

Author SHA1 Message Date
Haley Wang
7d7d17d192 add changelog 2025-02-10 14:08:32 +08:00
Evgeny Kuzin
0a8b4281e5 fix race using the same list from 2 goroutines 2025-02-07 11:55:45 -05:00
798 changed files with 65900 additions and 33743 deletions

View File

@@ -90,7 +90,7 @@ jobs:
- name: Publish coverage
uses: codecov/codecov-action@v5
with:
files: ./coverage.txt
file: ./coverage.txt
integration-test:
name: integration-test

View File

@@ -5,7 +5,6 @@ on:
- 'master'
paths:
- 'docs/**'
- '.github/workflows/docs.yaml'
workflow_dispatch: {}
permissions:
contents: read # This is required for actions/checkout and to commit back image update
@@ -18,41 +17,35 @@ jobs:
- name: Code checkout
uses: actions/checkout@v4
with:
path: __vm
path: main
- name: Checkout private code
uses: actions/checkout@v4
with:
repository: VictoriaMetrics/vmdocs
token: ${{ secrets.VM_BOT_GH_TOKEN }}
path: __vm-docs
path: docs
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
id: import-gpg
with:
gpg_private_key: ${{ secrets.VM_BOT_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.VM_BOT_PASSPHRASE }}
git_user_signingkey: true
git_commit_gpgsign: true
git_config_global: true
- name: Copy docs
id: update
workdir: docs
- name: Set short git commit SHA
id: vars
run: |
rsync -zarv \
--exclude="Makefile" \
docs/ ../__vm-docs/content/victoriametrics
echo "SHORT_SHA=$(git rev-parse --short $GITHUB_SHA)" >> $GITHUB_OUTPUT
working-directory: __vm
- name: Push to vmdocs
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "short_sha=$calculatedSha" >> $GITHUB_OUTPUT
working-directory: main
- name: update code and commit
run: |
rm -rf content
cp -r ../main/docs content
make clean-after-copy
git config --global user.name "${{ steps.import-gpg.outputs.email }}"
git config --global user.email "${{ steps.import-gpg.outputs.email }}"
if [[ -n $(git status --porcelain) ]]; then
git add .
git commit -S -m "sync docs with VictoriaMetrics/VictoriaMetrics commit: ${{ steps.update.outputs.SHORT_SHA }}"
git push
fi
working-directory: __vm-docs
git add .
git commit -S -m "sync docs with VictoriaMetrics/VictoriaMetrics commit: ${{ steps.vars.outputs.short_sha }}"
git push
working-directory: docs

View File

@@ -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.64.5
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.63.4
remove-golangci-lint:
rm -rf `which golangci-lint`

View File

@@ -3,7 +3,7 @@
![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)
![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)
![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/workflows/main/badge.svg?link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)
![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)
![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)
@@ -22,7 +22,7 @@ Here are some resources and information about VictoriaMetrics:
- Documentation: [docs.victoriametrics.com](https://docs.victoriametrics.com)
- Case studies: [Grammarly, Roblox, Wix,...](https://docs.victoriametrics.com/casestudies/).
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), docker images [Docker Hub](https://hub.docker.com/r/victoriametrics/victoria-metrics/) and [Quay](https://quay.io/repository/victoriametrics/victoria-metrics), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
- Available: [Binary releases](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest), [Docker images](https://hub.docker.com/r/victoriametrics/victoria-metrics/), [Source code](https://github.com/VictoriaMetrics/VictoriaMetrics)
- Deployment types: [Single-node version](https://docs.victoriametrics.com/), [Cluster version](https://docs.victoriametrics.com/cluster-victoriametrics/), and [Enterprise version](https://docs.victoriametrics.com/enterprise/)
- Changelog: [CHANGELOG](https://docs.victoriametrics.com/changelog/), and [How to upgrade](https://docs.victoriametrics.com/#how-to-upgrade-victoriametrics)
- Community: [Slack](https://slack.victoriametrics.com/), [X (Twitter)](https://x.com/VictoriaMetrics), [LinkedIn](https://www.linkedin.com/company/victoriametrics/), [YouTube](https://www.youtube.com/@VictoriaMetrics)

View File

@@ -8,7 +8,7 @@ The following versions of VictoriaMetrics receive regular security fixes:
|---------|--------------------|
| [latest release](https://docs.victoriametrics.com/changelog/) | :white_check_mark: |
| v1.102.x [LTS line](https://docs.victoriametrics.com/lts-releases/) | :white_check_mark: |
| v1.110.x [LTS line](https://docs.victoriametrics.com/lts-releases/) | :white_check_mark: |
| v1.97.x [LTS line](https://docs.victoriametrics.com/lts-releases/) | :white_check_mark: |
| other releases | :x: |
See [this page](https://victoriametrics.com/security/) for more details.

View File

@@ -60,7 +60,7 @@ func RequestHandler(path string, w http.ResponseWriter, r *http.Request) bool {
return true
}
switch path {
case "/", "":
case "/":
switch r.Method {
case http.MethodGet:
// Return fake response for Elasticsearch ping request.

View File

@@ -30,7 +30,7 @@ const (
var (
bodyBufferPool bytesutil.ByteBufferPool
allowedJournaldEntryNameChars = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*`)
allowedJournaldEntryNameChars = regexp.MustCompile(`^[A-Z_][A-Z0-9_]+`)
)
var (
@@ -129,11 +129,6 @@ func handleJournald(r *http.Request, w http.ResponseWriter) {
return
}
// systemd starting release v258 will support compression, which starts working after negotiation: it expects supported compression
// algorithms list in Accept-Encoding response header in a format "<algorithm_1>[:<priority_1>][;<algorithm_2>:<priority_2>]"
// See https://github.com/systemd/systemd/pull/34822
w.Header().Set("Accept-Encoding", "zstd")
// update requestJournaldDuration only for successfully parsed requests
// There is no need in updating requestJournaldDuration for request errors,
// since their timings are usually much smaller than the timing for successful request parsing.

View File

@@ -35,9 +35,9 @@ func TestPushJournaldOk(t *testing.T) {
)
// Parse binary data
f("__CURSOR=s=e0afe8412a6a49d2bfcf66aa7927b588;i=1f06;b=f778b6e2f7584a77b991a2366612a7b5;m=300bdfd420;t=62526e1182354;x=930dc44b370963b7\nE=JobStateChanged\n__REALTIME_TIMESTAMP=1729698775704404\n__MONOTONIC_TIMESTAMP=206357648416\n__SEQNUM=7942\n__SEQNUM_ID=e0afe8412a6a49d2bfcf66aa7927b588\n_BOOT_ID=f778b6e2f7584a77b991a2366612a7b5\n_UID=0\n_GID=0\n_MACHINE_ID=a4a970370c30a925df02a13c67167847\n_HOSTNAME=ecd5e4555787\n_RUNTIME_SCOPE=system\n_TRANSPORT=journal\n_CAP_EFFECTIVE=1ffffffffff\n_SYSTEMD_CGROUP=/init.scope\n_SYSTEMD_UNIT=init.scope\n_SYSTEMD_SLICE=-.slice\nCODE_FILE=<stdin>\nCODE_LINE=1\nCODE_FUNC=<module>\nSYSLOG_IDENTIFIER=python3\n_COMM=python3\n_EXE=/usr/bin/python3.12\n_CMDLINE=python3\nMESSAGE\n\x13\x00\x00\x00\x00\x00\x00\x00foo\nbar\n\n\nasda\nasda\n_PID=2763\n_SOURCE_REALTIME_TIMESTAMP=1729698775704375\n\n",
f("__CURSOR=s=e0afe8412a6a49d2bfcf66aa7927b588;i=1f06;b=f778b6e2f7584a77b991a2366612a7b5;m=300bdfd420;t=62526e1182354;x=930dc44b370963b7\n__REALTIME_TIMESTAMP=1729698775704404\n__MONOTONIC_TIMESTAMP=206357648416\n__SEQNUM=7942\n__SEQNUM_ID=e0afe8412a6a49d2bfcf66aa7927b588\n_BOOT_ID=f778b6e2f7584a77b991a2366612a7b5\n_UID=0\n_GID=0\n_MACHINE_ID=a4a970370c30a925df02a13c67167847\n_HOSTNAME=ecd5e4555787\n_RUNTIME_SCOPE=system\n_TRANSPORT=journal\n_CAP_EFFECTIVE=1ffffffffff\n_SYSTEMD_CGROUP=/init.scope\n_SYSTEMD_UNIT=init.scope\n_SYSTEMD_SLICE=-.slice\nCODE_FILE=<stdin>\nCODE_LINE=1\nCODE_FUNC=<module>\nSYSLOG_IDENTIFIER=python3\n_COMM=python3\n_EXE=/usr/bin/python3.12\n_CMDLINE=python3\nMESSAGE\n\x13\x00\x00\x00\x00\x00\x00\x00foo\nbar\n\n\nasda\nasda\n_PID=2763\n_SOURCE_REALTIME_TIMESTAMP=1729698775704375\n\n",
[]int64{1729698775704404000},
"{\"E\":\"JobStateChanged\",\"_BOOT_ID\":\"f778b6e2f7584a77b991a2366612a7b5\",\"_UID\":\"0\",\"_GID\":\"0\",\"_MACHINE_ID\":\"a4a970370c30a925df02a13c67167847\",\"_HOSTNAME\":\"ecd5e4555787\",\"_RUNTIME_SCOPE\":\"system\",\"_TRANSPORT\":\"journal\",\"_CAP_EFFECTIVE\":\"1ffffffffff\",\"_SYSTEMD_CGROUP\":\"/init.scope\",\"_SYSTEMD_UNIT\":\"init.scope\",\"_SYSTEMD_SLICE\":\"-.slice\",\"CODE_FILE\":\"\\u003cstdin>\",\"CODE_LINE\":\"1\",\"CODE_FUNC\":\"\\u003cmodule>\",\"SYSLOG_IDENTIFIER\":\"python3\",\"_COMM\":\"python3\",\"_EXE\":\"/usr/bin/python3.12\",\"_CMDLINE\":\"python3\",\"_msg\":\"foo\\nbar\\n\\n\\nasda\\nasda\",\"_PID\":\"2763\",\"_SOURCE_REALTIME_TIMESTAMP\":\"1729698775704375\"}",
"{\"_BOOT_ID\":\"f778b6e2f7584a77b991a2366612a7b5\",\"_UID\":\"0\",\"_GID\":\"0\",\"_MACHINE_ID\":\"a4a970370c30a925df02a13c67167847\",\"_HOSTNAME\":\"ecd5e4555787\",\"_RUNTIME_SCOPE\":\"system\",\"_TRANSPORT\":\"journal\",\"_CAP_EFFECTIVE\":\"1ffffffffff\",\"_SYSTEMD_CGROUP\":\"/init.scope\",\"_SYSTEMD_UNIT\":\"init.scope\",\"_SYSTEMD_SLICE\":\"-.slice\",\"CODE_FILE\":\"\\u003cstdin>\",\"CODE_LINE\":\"1\",\"CODE_FUNC\":\"\\u003cmodule>\",\"SYSLOG_IDENTIFIER\":\"python3\",\"_COMM\":\"python3\",\"_EXE\":\"/usr/bin/python3.12\",\"_CMDLINE\":\"python3\",\"_msg\":\"foo\\nbar\\n\\n\\nasda\\nasda\",\"_PID\":\"2763\",\"_SOURCE_REALTIME_TIMESTAMP\":\"1729698775704375\"}",
)
}

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

@@ -46,9 +46,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
}
switch {
case strings.HasPrefix(path, "/elasticsearch"):
// some clients may omit trailing slash
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8353
case strings.HasPrefix(path, "/elasticsearch/"):
path = strings.TrimPrefix(path, "/elasticsearch")
return elasticsearch.RequestHandler(path, w, r)
case strings.HasPrefix(path, "/loki/"):

View File

@@ -126,18 +126,6 @@ func pushFieldsFromScopeLogs(sc *pb.ScopeLogs, commonFields []logstorage.Field,
Value: attr.Value.FormatString(),
})
}
if len(lr.TraceID) > 0 {
fields = append(fields, logstorage.Field{
Name: "trace_id",
Value: lr.TraceID,
})
}
if len(lr.SpanID) > 0 {
fields = append(fields, logstorage.Field{
Name: "span_id",
Value: lr.SpanID,
})
}
fields = append(fields, logstorage.Field{
Name: "severity",
Value: lr.FormatSeverity(),

View File

@@ -18,7 +18,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
var (
@@ -306,7 +306,7 @@ type timeFlag struct {
}
func (tf *timeFlag) Set(s string) error {
msec, err := timeutil.ParseTimeMsec(s)
msec, err := promutils.ParseTimeMsec(s)
if err != nil {
return fmt.Errorf("cannot parse time from %q: %w", s, err)
}

View File

@@ -22,7 +22,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
// ProcessFacetsRequest handles /select/logsql/facets request.
@@ -116,7 +116,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
if stepStr == "" {
stepStr = "1d"
}
step, err := timeutil.ParseDuration(stepStr)
step, err := promutils.ParseDuration(stepStr)
if err != nil {
httpserver.Errorf(w, r, "cannot parse 'step' arg: %s", err)
return
@@ -131,7 +131,7 @@ func ProcessHitsRequest(ctx context.Context, w http.ResponseWriter, r *http.Requ
if offsetStr == "" {
offsetStr = "0s"
}
offset, err := timeutil.ParseDuration(offsetStr)
offset, err := promutils.ParseDuration(offsetStr)
if err != nil {
httpserver.Errorf(w, r, "cannot parse 'offset' arg: %s", err)
return
@@ -665,7 +665,7 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
if stepStr == "" {
stepStr = "1d"
}
step, err := timeutil.ParseDuration(stepStr)
step, err := promutils.ParseDuration(stepStr)
if err != nil {
err = fmt.Errorf("cannot parse 'step' arg: %s", err)
httpserver.SendPrometheusError(w, r, err)
@@ -688,22 +688,19 @@ 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 {
// Do not move q.GetTimestamp() outside writeBlock, since ts
// must be initialized to query timestamp for every processed log row.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8312
ts := q.GetTimestamp()
labels := make([]logstorage.Field, 0, len(byFields))
for j, c := range columns {
if c.Name == "_time" {
nsec, ok := logstorage.TryParseTimestampRFC3339Nano(c.Values[i])
if ok {
ts = nsec
timestamp = nsec
continue
}
}
@@ -724,7 +721,7 @@ func ProcessStatsQueryRangeRequest(ctx context.Context, w http.ResponseWriter, r
dst = logstorage.MarshalFieldsToJSON(dst, labels)
key := string(dst)
p := statsPoint{
Timestamp: ts,
Timestamp: timestamp,
Value: strings.Clone(c.Values[i]),
}
@@ -1122,7 +1119,7 @@ func getTimeNsec(r *http.Request, argName string) (int64, bool, error) {
return 0, false, nil
}
currentTimestamp := time.Now().UnixNano()
nsecs, err := timeutil.ParseTimeAt(s, currentTimestamp)
nsecs, err := promutils.ParseTimeAt(s, currentTimestamp)
if err != nil {
return 0, false, fmt.Errorf("cannot parse %s=%s: %w", argName, s, err)
}

View File

@@ -176,62 +176,50 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
func processSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) bool {
httpserver.EnableCORS(w, r)
startTime := time.Now()
switch path {
case "/select/logsql/facets":
logsqlFacetsRequests.Inc()
logsql.ProcessFacetsRequest(ctx, w, r)
logsqlFacetsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/field_names":
logsqlFieldNamesRequests.Inc()
logsql.ProcessFieldNamesRequest(ctx, w, r)
logsqlFieldNamesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/field_values":
logsqlFieldValuesRequests.Inc()
logsql.ProcessFieldValuesRequest(ctx, w, r)
logsqlFieldValuesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/hits":
logsqlHitsRequests.Inc()
logsql.ProcessHitsRequest(ctx, w, r)
logsqlHitsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/query":
logsqlQueryRequests.Inc()
logsql.ProcessQueryRequest(ctx, w, r)
logsqlQueryDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stats_query":
logsqlStatsQueryRequests.Inc()
logsql.ProcessStatsQueryRequest(ctx, w, r)
logsqlStatsQueryDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stats_query_range":
logsqlStatsQueryRangeRequests.Inc()
logsql.ProcessStatsQueryRangeRequest(ctx, w, r)
logsqlStatsQueryRangeDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_field_names":
logsqlStreamFieldNamesRequests.Inc()
logsql.ProcessStreamFieldNamesRequest(ctx, w, r)
logsqlStreamFieldNamesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_field_values":
logsqlStreamFieldValuesRequests.Inc()
logsql.ProcessStreamFieldValuesRequest(ctx, w, r)
logsqlStreamFieldValuesDuration.UpdateDuration(startTime)
return true
case "/select/logsql/stream_ids":
logsqlStreamIDsRequests.Inc()
logsql.ProcessStreamIDsRequest(ctx, w, r)
logsqlStreamIDsDuration.UpdateDuration(startTime)
return true
case "/select/logsql/streams":
logsqlStreamsRequests.Inc()
logsql.ProcessStreamsRequest(ctx, w, r)
logsqlStreamsDuration.UpdateDuration(startTime)
return true
default:
return false
@@ -252,39 +240,16 @@ func getMaxQueryDuration(r *http.Request) time.Duration {
}
var (
logsqlFacetsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/facets"}`)
logsqlFacetsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/facets"}`)
logsqlFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_names"}`)
logsqlFieldNamesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/field_names"}`)
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
logsqlFieldValuesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/field_values"}`)
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
logsqlHitsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/hits"}`)
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
logsqlQueryDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/query"}`)
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
logsqlStatsQueryDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stats_query"}`)
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
logsqlStatsQueryRangeDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stats_query_range"}`)
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
logsqlStreamFieldNamesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_field_names"}`)
logsqlFacetsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/facets"}`)
logsqlFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_names"}`)
logsqlFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/field_values"}`)
logsqlHitsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/hits"}`)
logsqlQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/query"}`)
logsqlStatsQueryRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query"}`)
logsqlStatsQueryRangeRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stats_query_range"}`)
logsqlStreamFieldNamesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_names"}`)
logsqlStreamFieldValuesRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_field_values"}`)
logsqlStreamFieldValuesDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_field_values"}`)
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
logsqlStreamIDsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/stream_ids"}`)
logsqlStreamsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/streams"}`)
logsqlStreamsDuration = metrics.NewSummary(`vl_http_request_duration_seconds{path="/select/logsql/streams"}`)
// no need to track duration for tail requests, as they usually take long time
logsqlTailRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/tail"}`)
logsqlStreamIDsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/stream_ids"}`)
logsqlStreamsRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/streams"}`)
logsqlTailRequests = metrics.NewCounter(`vl_http_requests_total{path="/select/logsql/tail"}`)
)

View File

@@ -0,0 +1,12 @@
{
"files": {
"main.css": "./static/css/main.02a1c6cb.css",
"main.js": "./static/js/main.55c8060b.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.02a1c6cb.css",
"static/js/main.55c8060b.js"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.uplot,.uplot *,.uplot *:before,.uplot *:after{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5;width:min-content}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{position:relative;-webkit-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;position:relative;width:100%;height:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:#00000012;position:absolute;pointer-events:none}.u-cursor-x,.u-cursor-y{position:absolute;left:0;top:0;pointer-events:none;will-change:transform}.u-hz .u-cursor-x,.u-vt .u-cursor-y{height:100%;border-right:1px dashed #607D8B}.u-hz .u-cursor-y,.u-vt .u-cursor-x{width:100%;border-bottom:1px dashed #607D8B}.u-cursor-pt{position:absolute;top:0;left:0;border-radius:50%;border:0 solid;pointer-events:none;will-change:transform;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}

File diff suppressed because one or more lines are too long

View File

@@ -1,57 +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"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json"/>
<!--
Notice the use of in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>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 type="module" crossorigin src="./assets/index-DuTUAk-m.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-CEiptoJw.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
<!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>

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

@@ -0,0 +1,38 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @remix-run/router v1.19.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router DOM v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

View File

@@ -9,7 +9,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/streamaggr"
"github.com/VictoriaMetrics/metrics"
)
@@ -54,7 +53,7 @@ func TestGetLabelsHash_Distribution(t *testing.T) {
}
func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
f := func(streamAggrConfig, relabelConfig string, enableWindows bool, dedupInterval time.Duration, keepInput, dropInput bool, input string) {
f := func(streamAggrConfig, relabelConfig string, dedupInterval time.Duration, keepInput, dropInput bool, input string) {
t.Helper()
perURLRelabel, err := promrelabel.ParseRelabelConfigsData([]byte(relabelConfig))
if err != nil {
@@ -78,15 +77,12 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
rowsDroppedByRelabel: metrics.GetOrCreateCounter(`bar`),
}
if dedupInterval > 0 {
rwctx.deduplicator = streamaggr.NewDeduplicator(nil, enableWindows, dedupInterval, nil, "dedup-global")
rwctx.deduplicator = streamaggr.NewDeduplicator(nil, dedupInterval, nil, "dedup-global")
}
if streamAggrConfig != "" {
pushNoop := func(_ []prompbmarshal.TimeSeries) {}
opts := streamaggr.Options{
EnableWindows: enableWindows,
}
sas, err := streamaggr.LoadFromData([]byte(streamAggrConfig), pushNoop, &opts, "global")
sas, err := streamaggr.LoadFromData([]byte(streamAggrConfig), pushNoop, nil, "global")
if err != nil {
t.Fatalf("cannot load streamaggr configs: %s", err)
}
@@ -95,7 +91,7 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
}
offsetMsecs := time.Now().UnixMilli()
inputTss := prometheus.MustParsePromMetrics(input, offsetMsecs)
inputTss := prompbmarshal.MustParsePromMetrics(input, offsetMsecs)
expectedTss := make([]prompbmarshal.TimeSeries, len(inputTss))
// copy inputTss to make sure it is not mutated during TryPush call
@@ -118,13 +114,13 @@ func TestRemoteWriteContext_TryPush_ImmutableTimeseries(t *testing.T) {
- action: keep
source_labels: [env]
regex: "dev"
`, false, 0, false, false, `
`, 0, false, false, `
metric{env="dev"} 10
metric{env="bar"} 20
metric{env="dev"} 15
metric{env="bar"} 25
`)
f(``, ``, true, time.Hour, false, false, `
f(``, ``, time.Hour, false, false, `
metric{env="dev"} 10
metric{env="foo"} 20
metric{env="dev"} 15
@@ -134,7 +130,7 @@ metric{env="foo"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, true, time.Hour, false, false, `
`, time.Hour, false, false, `
metric{env="dev"} 10
metric{env="bar"} 20
metric{env="dev"} 15
@@ -144,7 +140,7 @@ metric{env="bar"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, true, time.Hour, true, false, `
`, time.Hour, true, false, `
metric{env="test"} 10
metric{env="dev"} 20
metric{env="foo"} 15
@@ -154,7 +150,7 @@ metric{env="dev"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, true, time.Hour, false, true, `
`, time.Hour, false, true, `
metric{env="foo"} 10
metric{env="dev"} 20
metric{env="foo"} 15
@@ -164,7 +160,7 @@ metric{env="dev"} 25
- action: keep
source_labels: [env]
regex: "dev"
`, true, time.Hour, true, true, `
`, time.Hour, true, true, `
metric{env="dev"} 10
metric{env="test"} 20
metric{env="dev"} 15

View File

@@ -35,9 +35,6 @@ var (
"clients pushing data into the vmagent. See https://docs.victoriametrics.com/stream-aggregation/#ignore-aggregation-intervals-on-start")
streamAggrGlobalDropInputLabels = flagutil.NewArrayString("streamAggr.dropInputLabels", "An optional list of labels to drop from samples for aggregator "+
"before stream de-duplication and aggregation . See https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels")
streamAggrGlobalEnableWindows = flag.Bool("streamAggr.enableWindows", false, "Enables aggregation within fixed windows for all global aggregators. "+
"This allows to get more precise results, but impacts resource usage as it requires twice more memory to store two states. "+
"See https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows.")
// Per URL config
streamAggrConfig = flagutil.NewArrayString("remoteWrite.streamAggr.config", "Optional path to file with stream aggregation config for the corresponding -remoteWrite.url. "+
@@ -62,9 +59,6 @@ var (
"before stream de-duplication and aggregation with -remoteWrite.streamAggr.config and -remoteWrite.streamAggr.dedupInterval at the corresponding -remoteWrite.url. "+
"Multiple labels per remoteWrite.url must be delimited by '^^': -remoteWrite.streamAggr.dropInputLabels='replica^^az,replica'. "+
"See https://docs.victoriametrics.com/stream-aggregation/#dropping-unneeded-labels")
streamAggrEnableWindows = flagutil.NewArrayBool("remoteWrite.streamAggr.enableWindows", "Enables aggregation within fixed windows for all remote write's aggregators. "+
"This allows to get more precise results, but impacts resource usage as it requires twice more memory to store two states. "+
"See https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows.")
)
// CheckStreamAggrConfigs checks -remoteWrite.streamAggr.config and -streamAggr.config.
@@ -141,7 +135,7 @@ func initStreamAggrConfigGlobal() {
}
dedupInterval := *streamAggrGlobalDedupInterval
if dedupInterval > 0 {
deduplicatorGlobal = streamaggr.NewDeduplicator(pushToRemoteStoragesTrackDropped, *streamAggrGlobalEnableWindows, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
deduplicatorGlobal = streamaggr.NewDeduplicator(pushToRemoteStoragesTrackDropped, dedupInterval, *streamAggrGlobalDropInputLabels, "dedup-global")
}
}
@@ -167,7 +161,7 @@ func (rwctx *remoteWriteCtx) initStreamAggrConfig() {
if streamAggrDropInputLabels.GetOptionalArg(idx) != "" {
dropLabels = strings.Split(streamAggrDropInputLabels.GetOptionalArg(idx), "^^")
}
rwctx.deduplicator = streamaggr.NewDeduplicator(rwctx.pushInternalTrackDropped, *streamAggrGlobalEnableWindows, dedupInterval, dropLabels, alias)
rwctx.deduplicator = streamaggr.NewDeduplicator(rwctx.pushInternalTrackDropped, dedupInterval, dropLabels, alias)
}
}
@@ -213,7 +207,6 @@ func newStreamAggrConfigGlobal() (*streamaggr.Aggregators, error) {
IgnoreOldSamples: *streamAggrGlobalIgnoreOldSamples,
IgnoreFirstIntervals: *streamAggrGlobalIgnoreFirstIntervals,
KeepInput: *streamAggrGlobalKeepInput,
EnableWindows: *streamAggrGlobalEnableWindows,
}
sas, err := streamaggr.LoadFromFile(path, pushToRemoteStoragesTrackDropped, opts, "global")
@@ -247,7 +240,6 @@ func newStreamAggrConfigPerURL(idx int, pushFunc streamaggr.PushFunc) (*streamag
IgnoreOldSamples: streamAggrIgnoreOldSamples.GetOptionalArg(idx),
IgnoreFirstIntervals: streamAggrIgnoreFirstIntervals.GetOptionalArg(idx),
KeepInput: streamAggrKeepInput.GetOptionalArg(idx),
EnableWindows: streamAggrEnableWindows.GetOptionalArg(idx),
}
sas, err := streamaggr.LoadFromFile(path, pushFunc, opts, alias)

View File

@@ -51,11 +51,6 @@ Examples:
Usage: `Optional external URL to template in rule's labels or annotations.`,
Required: false,
},
&cli.StringFlag{
Name: "httpListenPort",
Usage: `Optional local port for incoming HTTP requests. If not specified, a random unoccupied port will be used.`,
Required: false,
},
&cli.StringFlag{
Name: "loggerLevel",
Usage: `Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC (default "ERROR").`,
@@ -63,7 +58,7 @@ Examples:
},
},
Action: func(c *cli.Context) error {
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("httpListenPort"), c.String("loggerLevel")); failed {
if failed := unittest.UnitTest(c.StringSlice("files"), c.Bool("disableAlertgroupLabel"), c.StringSlice("external.label"), c.String("external.url"), c.String("loggerLevel")); failed {
return fmt.Errorf("unittest failed")
}
return nil

View File

@@ -4,18 +4,13 @@ import (
"context"
"flag"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/signal"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v2"
@@ -33,6 +28,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -42,9 +38,15 @@ import (
var (
storagePath string
httpListenAddr string
httpListenAddr = ":8880"
// insert series from 1970-01-01T00:00:00
testStartTime = time.Unix(0, 0).UTC()
testStartTime = time.Unix(0, 0).UTC()
testPromWriteHTTPPath = "http://127.0.0.1" + httpListenAddr + "/api/v1/write"
testDataSourcePath = "http://127.0.0.1" + httpListenAddr + "/prometheus"
testRemoteWritePath = "http://127.0.0.1" + httpListenAddr
testHealthHTTPPath = "http://127.0.0.1" + httpListenAddr + "/health"
testLogLevel = "ERROR"
disableAlertgroupLabel bool
)
@@ -54,7 +56,7 @@ const (
)
// UnitTest runs unittest for files
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, httpListenPort, logLevel string) bool {
func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, externalURL, logLevel string) bool {
if logLevel != "" {
testLogLevel = logLevel
}
@@ -65,42 +67,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
if err := templates.Load([]string{}, *eu); err != nil {
logger.Fatalf("failed to load template: %v", err)
}
// set up http server
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/prometheus/api/v1/query":
if err := prometheus.QueryHandler(nil, time.Now(), w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
case "/prometheus/api/v1/write", "/api/v1/write":
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
default:
}
})
if httpListenPort == "" {
server := httptest.NewServer(handler)
httpListenAddr = strings.Split(server.URL, ":")[2]
defer server.Close()
} else {
httpListenAddr = httpListenPort
ln, err := net.Listen("tcp", fmt.Sprintf(":%s", httpListenPort))
if err != nil {
logger.Fatalf("cannot listen on port %s: %v", httpListenPort, err)
}
go func() {
err = http.Serve(ln, handler)
if err != nil {
logger.Fatalf("cannot start http server: %v", err)
}
defer ln.Close()
}()
}
// adding time.Now().UnixNano() to avoid possible file conflict when multiple processes run on a single host
storagePath = filepath.Join(os.TempDir(), testStoragePath, strconv.FormatInt(time.Now().UnixNano(), 10))
storagePath = filepath.Join(os.TempDir(), testStoragePath)
processFlags()
vminsert.Init()
vmselect.Init()
@@ -135,34 +102,16 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
}
var failed bool
runTest := func() bool {
for fileName, file := range testfiles {
if err := ruleUnitTest(fileName, file, labels); err != nil {
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
return true
}
fmt.Println("SUCCESS")
for fileName, file := range testfiles {
if err := ruleUnitTest(fileName, file, labels); err != nil {
fmt.Println("FAILED")
fmt.Printf("failed to run unit test for file %q: \n%v", fileName, err)
failed = true
} else {
fmt.Println(" SUCCESS")
}
return false
}
finishCh := make(chan struct{}, 1)
go func() {
failed = runTest()
finishCh <- struct{}{}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-sigs:
fmt.Printf("received signal %s\n", sig)
failed = true
break
case <-finishCh:
break
}
return failed
}
@@ -262,8 +211,8 @@ func processFlags() {
{flag: "search.disableCache", value: "true"},
// set storage retention time to 100 years, allow to store series from 1970-01-01T00:00:00.
{flag: "retentionPeriod", value: "100y"},
{flag: "datasource.url", value: fmt.Sprintf("http://127.0.0.1:%s/prometheus", httpListenAddr)},
{flag: "remoteWrite.url", value: fmt.Sprintf("http://127.0.0.1:%s", httpListenAddr)},
{flag: "datasource.url", value: testDataSourcePath},
{flag: "remoteWrite.url", value: testRemoteWritePath},
{flag: "notifier.blackhole", value: "true"},
} {
// panics if flag doesn't exist
@@ -275,10 +224,27 @@ func processFlags() {
func setUp() {
vmstorage.Init(promql.ResetRollupResultCacheIfNeeded)
var ab flagutil.ArrayBool
go httpserver.Serve([]string{httpListenAddr}, &ab, func(w http.ResponseWriter, r *http.Request) bool {
switch r.URL.Path {
case "/prometheus/api/v1/query":
if err := prometheus.QueryHandler(nil, time.Now(), w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
return true
case "/prometheus/api/v1/write", "/api/v1/write":
if err := promremotewrite.InsertHandler(r); err != nil {
httpserver.Errorf(w, r, "%s", err)
}
return true
default:
}
return false
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
readyCheckFunc := func() bool {
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/health", httpListenAddr))
resp, err := http.Get(testHealthHTTPPath)
if err != nil {
return false
}
@@ -300,6 +266,9 @@ checkCheck:
}
func tearDown() {
if err := httpserver.Stop([]string{httpListenAddr}); err != nil {
logger.Errorf("cannot stop the webservice: %s", err)
}
vmstorage.Stop()
metrics.UnregisterAllMetrics()
fs.MustRemoveAll(storagePath)
@@ -314,7 +283,7 @@ func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]i
if tg.Interval == nil {
tg.Interval = promutils.NewDuration(evalInterval)
}
err := writeInputSeries(tg.InputSeries, tg.Interval, testStartTime, fmt.Sprintf("http://127.0.0.1:%s/api/v1/write", httpListenAddr))
err := writeInputSeries(tg.InputSeries, tg.Interval, testStartTime, testPromWriteHTTPPath)
if err != nil {
return []error{err}
}

View File

@@ -8,7 +8,7 @@ func TestUnitTest_Failure(t *testing.T) {
f := func(files []string) {
t.Helper()
failed := UnitTest(files, false, nil, "", "", "")
failed := UnitTest(files, false, nil, "", "")
if !failed {
t.Fatalf("expecting failed test")
}
@@ -20,20 +20,19 @@ func TestUnitTest_Failure(t *testing.T) {
}
func TestUnitTest_Success(t *testing.T) {
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL, httpPort string) {
f := func(disableGroupLabel bool, files []string, externalLabels []string, externalURL string) {
t.Helper()
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, httpPort, "")
failed := UnitTest(files, disableGroupLabel, externalLabels, externalURL, "")
if failed {
t.Fatalf("unexpected failed test")
}
}
// run multi files with random http port
f(false, []string{"./testdata/test1.yaml", "./testdata/test2.yaml"}, []string{"cluster=prod"}, "http://grafana:3000", "")
// run multi files
f(false, []string{"./testdata/test1.yaml", "./testdata/test2.yaml"}, []string{"cluster=prod"}, "http://grafana:3000")
// disable group label
// template with null external values
// specify httpListenAddr
f(true, []string{"./testdata/disable-group-label.yaml"}, nil, "", "8880")
f(true, []string{"./testdata/disable-group-label.yaml"}, nil, "")
}

View File

@@ -157,19 +157,6 @@ func TestGroupValidate_Failure(t *testing.T) {
f(&Group{}, false, "group name must be set")
f(&Group{
Name: "both record and alert are not set",
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
f(&Group{
Name: "negative interval",
Interval: promutils.NewDuration(-1),
@@ -253,6 +240,59 @@ func TestGroupValidate_Failure(t *testing.T) {
},
}, false, "duplicate")
f(&Group{
Name: "test graphite with prometheus expr",
Type: NewGraphiteType(),
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
f(&Group{
Name: "test graphite inherit",
Type: NewGraphiteType(),
Rules: []Rule{
{
Expr: "sumSeries(time('foo.bar',10))",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sum(up == 0 ) by (host)",
},
},
}, false, "either `record` or `alert` must be set")
f(&Group{
Name: "test vlogs with prometheus expr",
Type: NewVLogsType(),
Rules: []Rule{
{
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Expr: "sumSeries(time('foo.bar',10))",
},
},
}, false, "invalid rule")
// validate expressions
f(&Group{
Name: "test",
Rules: []Rule{
{
Record: "record",
Expr: "up | 0",
},
},
}, true, "invalid expression")
f(&Group{
Name: "test thanos",
Type: NewRawType("thanos"),
@@ -263,20 +303,8 @@ func TestGroupValidate_Failure(t *testing.T) {
},
}, true, "unknown datasource type")
// validate expressions
f(&Group{
Name: "test prometheus expr",
Type: NewPrometheusType(),
Rules: []Rule{
{
Record: "record",
Expr: "up | 0",
},
},
}, true, "bad prometheus expr")
f(&Group{
Name: "test graphite expr",
Name: "test graphite",
Type: NewGraphiteType(),
Rules: []Rule{
{Alert: "alert", Expr: "up == 1", Labels: map[string]string{
@@ -286,63 +314,14 @@ func TestGroupValidate_Failure(t *testing.T) {
}, true, "bad graphite expr")
f(&Group{
Name: "test vlogs expr",
Name: "test vlogs",
Type: NewVLogsType(),
Rules: []Rule{
{Alert: "alert", Expr: "stats count(*) as requests"},
{Alert: "alert", Expr: "stats count(*) as requests", Labels: map[string]string{
"description": "some-description",
}},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test vlogs expr",
Type: NewVLogsType(),
Rules: []Rule{
{Alert: "alert", Expr: "_time: 1m | stats by (path, _time: 1m) count(*) as requests"},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test graphite with prometheus expr",
Type: NewGraphiteType(),
Rules: []Rule{
{
Record: "r1",
ID: 1,
Expr: "sumSeries(time('foo.bar',10))",
For: promutils.NewDuration(10 * time.Millisecond),
},
{
Record: "r2",
ID: 2,
Expr: "sum(up == 0 ) by (host)",
},
},
}, true, "bad graphite expr")
f(&Group{
Name: "test vlogs with prometheus exp",
Type: NewVLogsType(),
Rules: []Rule{
{
Record: "r1",
Expr: "sum(up == 0 ) by (host)",
For: promutils.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad LogsQL expr")
f(&Group{
Name: "test prometheus with vlogs exp",
Type: NewPrometheusType(),
Rules: []Rule{
{
Record: "r1",
Expr: "* | stats by (path) count()",
For: promutils.NewDuration(10 * time.Millisecond),
},
},
}, true, "bad prometheus expr")
}
func TestGroupValidate_Success(t *testing.T) {

View File

@@ -71,18 +71,9 @@ func (t *Type) ValidateExpr(expr string) error {
return fmt.Errorf("bad prometheus expr: %q, err: %w", expr, err)
}
case "vlogs":
q, err := logstorage.ParseStatsQuery(expr, 0)
if err != nil {
if _, err := logstorage.ParseStatsQuery(expr, 0); err != nil {
return fmt.Errorf("bad LogsQL expr: %q, err: %w", expr, err)
}
fields, _ := q.GetStatsByFields()
for i := range fields {
// VictoriaLogs inserts `_time` field as a label in result when query with `stats by (_time:step)`,
// making the result meaningless and may lead to cardinality issues.
if fields[i] == "_time" {
return fmt.Errorf("bad LogsQL expr: %q, err: cannot contain time buckets stats pipe `stats by (_time:step)`", expr)
}
}
default:
return fmt.Errorf("unknown datasource type=%q", t.Name)
}

View File

@@ -405,9 +405,6 @@ func configsEqual(a, b []config.Group) bool {
if a[i].Checksum != b[i].Checksum {
return false
}
if a[i].File != b[i].File {
return false
}
}
return true
}

View File

@@ -10,8 +10,6 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
@@ -31,34 +29,25 @@ type AlertManager struct {
// stores already parsed RelabelConfigs object
relabelConfigs *promrelabel.ParsedConfigs
metrics *notifierMetrics
metrics *metrics
}
type notifierMetrics struct {
set *metrics.Set
alertsSent *metrics.Counter
alertsSendErrors *metrics.Counter
type metrics struct {
alertsSent *utils.Counter
alertsSendErrors *utils.Counter
}
func newNotifierMetrics(addr string) *notifierMetrics {
set := metrics.NewSet()
metrics.RegisterSet(set)
return &notifierMetrics{
set: set,
alertsSent: set.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_sent_total{addr=%q}", addr)),
alertsSendErrors: set.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_send_errors_total{addr=%q}", addr)),
func newMetrics(addr string) *metrics {
return &metrics{
alertsSent: utils.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_sent_total{addr=%q}", addr)),
alertsSendErrors: utils.GetOrCreateCounter(fmt.Sprintf("vmalert_alerts_send_errors_total{addr=%q}", addr)),
}
}
func (nm *notifierMetrics) close() {
metrics.UnregisterSet(nm.set, true)
}
// Close is a destructor method for AlertManager
func (am *AlertManager) Close() {
am.metrics.close()
am.metrics.alertsSent.Unregister()
am.metrics.alertsSendErrors.Unregister()
}
// Addr returns address where alerts are sent.
@@ -81,7 +70,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 := make([]Alert, 0, len(alerts))
alertsToSend := alerts[:0]
lblss := make([][]prompbmarshal.Label, 0, len(alerts))
for _, a := range alerts {
lbls := a.applyRelabelingIfNeeded(am.relabelConfigs)
@@ -191,6 +180,6 @@ func NewAlertManager(alertManagerURL string, fn AlertURLGenerator, authCfg proma
relabelConfigs: relabelCfg,
client: &http.Client{Transport: tr},
timeout: timeout,
metrics: newNotifierMetrics(alertManagerURL),
metrics: newMetrics(alertManagerURL),
}, nil
}

View File

@@ -54,7 +54,6 @@ var (
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
oauth2Scopes = flagutil.NewArrayString("notifier.oauth2.scopes", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'. "+
"If multiple args are set, then they are applied independently for the corresponding -notifier.url")
sendTimeout = flagutil.NewArrayDuration("notifier.sendTimeout", 10*time.Second, "Timeout when sending alerts to the corresponding -notifier.url")
)
// cw holds a configWatcher for configPath configuration file
@@ -176,7 +175,7 @@ func notifiersFromFlags(gen AlertURLGenerator) ([]Notifier, error) {
}
addr = strings.TrimSuffix(addr, "/")
am, err := NewAlertManager(addr+alertManagerPath, gen, authCfg, nil, sendTimeout.GetOptionalArg(i))
am, err := NewAlertManager(addr+alertManagerPath, gen, authCfg, nil, time.Second*10)
if err != nil {
return nil, err
}

View File

@@ -6,7 +6,7 @@ import "context"
// to be sent.
type blackHoleNotifier struct {
addr string
metrics *notifierMetrics
metrics *metrics
}
// Send will send no notifications, but increase the metric.
@@ -22,7 +22,8 @@ func (bh blackHoleNotifier) Addr() string {
// Close unregister the metrics
func (bh *blackHoleNotifier) Close() {
bh.metrics.close()
bh.metrics.alertsSent.Unregister()
bh.metrics.alertsSendErrors.Unregister()
}
// newBlackHoleNotifier creates a new blackHoleNotifier
@@ -30,6 +31,6 @@ func newBlackHoleNotifier() *blackHoleNotifier {
address := "blackhole"
return &blackHoleNotifier{
addr: address,
metrics: newNotifierMetrics(address),
metrics: newMetrics(address),
}
}

View File

@@ -9,8 +9,6 @@ import (
"sync"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -59,69 +57,6 @@ type alertingRuleMetrics struct {
seriesFetched *utils.Gauge
}
func newAlertingRuleMetrics(set *metrics.Set, ar *AlertingRule) *alertingRuleMetrics {
labels := fmt.Sprintf(`alertname=%q, group=%q, file=%q, id="%d"`, ar.Name, ar.GroupName, ar.File, ar.ID())
arm := &alertingRuleMetrics{}
arm.pending = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerts_pending{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StatePending {
num++
}
}
return float64(num)
})
arm.active = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerts_firing{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StateFiring {
num++
}
}
return float64(num)
})
arm.errors = utils.NewCounter(set, fmt.Sprintf(`vmalert_alerting_rules_errors_total{%s}`, labels))
arm.samples = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := ar.state.getLast()
return float64(e.Samples)
})
arm.seriesFetched = utils.NewGauge(set, fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_series_fetched{%s}`, labels),
func() float64 {
e := ar.state.getLast()
if e.SeriesFetched == nil {
// means seriesFetched is unsupported
return -1
}
seriesFetched := float64(*e.SeriesFetched)
if seriesFetched == 0 && e.Samples > 0 {
// `alert: 0.95` will fetch no series
// but will get one time series in response.
seriesFetched = float64(e.Samples)
}
return seriesFetched
})
return arm
}
func (arm *alertingRuleMetrics) close() {
if arm == nil {
return
}
arm.errors.Unregister()
arm.active.Unregister()
arm.pending.Unregister()
arm.samples.Unregister()
arm.seriesFetched.Unregister()
}
// NewAlertingRule creates a new AlertingRule
func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *AlertingRule {
ar := &AlertingRule{
@@ -146,7 +81,8 @@ func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
Headers: group.Headers,
Debug: cfg.Debug,
}),
alerts: make(map[uint64]*notifier.Alert),
alerts: make(map[uint64]*notifier.Alert),
metrics: &alertingRuleMetrics{},
}
entrySize := *ruleUpdateEntriesLimit
@@ -159,17 +95,63 @@ func NewAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
ar.state = &ruleState{
entries: make([]StateEntry, entrySize),
}
ar.metrics = newAlertingRuleMetrics(group.metrics.set, ar)
labels := fmt.Sprintf(`alertname=%q, group=%q, file=%q, id="%d"`, ar.Name, group.Name, group.File, ar.ID())
ar.metrics.pending = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerts_pending{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StatePending {
num++
}
}
return float64(num)
})
ar.metrics.active = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerts_firing{%s}`, labels),
func() float64 {
ar.alertsMu.RLock()
defer ar.alertsMu.RUnlock()
var num int
for _, a := range ar.alerts {
if a.State == notifier.StateFiring {
num++
}
}
return float64(num)
})
ar.metrics.errors = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_alerting_rules_errors_total{%s}`, labels))
ar.metrics.samples = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := ar.state.getLast()
return float64(e.Samples)
})
ar.metrics.seriesFetched = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_alerting_rules_last_evaluation_series_fetched{%s}`, labels),
func() float64 {
e := ar.state.getLast()
if e.SeriesFetched == nil {
// means seriesFetched is unsupported
return -1
}
seriesFetched := float64(*e.SeriesFetched)
if seriesFetched == 0 && e.Samples > 0 {
// `alert: 0.95` will fetch no series
// but will get one time series in response.
seriesFetched = float64(e.Samples)
}
return seriesFetched
})
return ar
}
func (ar *AlertingRule) registerMetrics(g *Group) {
ar.metrics = newAlertingRuleMetrics(g.metrics.set, ar)
}
// close unregisters rule metrics
func (ar *AlertingRule) unregisterMetrics() {
ar.metrics.close()
func (ar *AlertingRule) close() {
ar.metrics.active.Unregister()
ar.metrics.pending.Unregister()
ar.metrics.errors.Unregister()
ar.metrics.samples.Unregister()
ar.metrics.seriesFetched.Unregister()
}
// String implements Stringer interface

View File

@@ -11,8 +11,6 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -1250,17 +1248,13 @@ func newTestAlertingRule(name string, waitFor time.Duration) *AlertingRule {
EvalInterval: waitFor,
alerts: make(map[uint64]*notifier.Alert),
state: &ruleState{entries: make([]StateEntry, 10)},
metrics: getTestAlertingRuleMetrics(name),
metrics: &alertingRuleMetrics{
errors: utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_alerting_rules_errors_total{alertname=%q}`, name)),
},
}
return &rule
}
func getTestAlertingRuleMetrics(name string) *alertingRuleMetrics {
m := &alertingRuleMetrics{}
m.errors = utils.NewCounter(metrics.NewSet(), fmt.Sprintf(`vmalert_alerting_rules_errors_total{alertname=%q}`, name))
return m
}
func newTestAlertingRuleWithCustomFields(name string, waitFor, evalInterval, keepFiringFor time.Duration, annotation map[string]string) *AlertingRule {
rule := newTestAlertingRule(name, waitFor)
if evalInterval != 0 {

View File

@@ -13,8 +13,6 @@ import (
"github.com/cheggaaa/pb/v3"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
@@ -22,6 +20,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/metrics"
)
var (
@@ -75,23 +74,19 @@ type Group struct {
}
type groupMetrics struct {
set *metrics.Set
iterationTotal *metrics.Counter
iterationDuration *metrics.Summary
iterationMissed *metrics.Counter
iterationInterval *metrics.Gauge
iterationTotal *utils.Counter
iterationDuration *utils.Summary
iterationMissed *utils.Counter
iterationInterval *utils.Gauge
}
func newGroupMetrics(g *Group) *groupMetrics {
m := &groupMetrics{}
m.set = metrics.NewSet()
labels := fmt.Sprintf(`group=%q, file=%q`, g.Name, g.File)
m.iterationTotal = m.set.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
m.iterationDuration = m.set.GetOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
m.iterationMissed = m.set.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
m.iterationInterval = m.set.GetOrCreateGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
m.iterationTotal = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_total{%s}`, labels))
m.iterationDuration = utils.GetOrCreateSummary(fmt.Sprintf(`vmalert_iteration_duration_seconds{%s}`, labels))
m.iterationMissed = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_iteration_missed_total{%s}`, labels))
m.iterationInterval = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_iteration_interval_seconds{%s}`, labels), func() float64 {
g.mu.RLock()
i := g.Interval.Seconds()
g.mu.RUnlock()
@@ -100,14 +95,6 @@ func newGroupMetrics(g *Group) *groupMetrics {
return m
}
func (m *groupMetrics) start() {
metrics.RegisterSet(m.set)
}
func (m *groupMetrics) close() {
metrics.UnregisterSet(m.set, true)
}
// merges group rule labels into result map
// set2 has priority over set1.
func mergeLabels(groupName, ruleName string, set1, set2 map[string]string) map[string]string {
@@ -250,7 +237,7 @@ func (g *Group) updateWith(newGroup *Group) error {
if !ok {
// old rule is not present in the new list
// so we mark it for removing
g.Rules[i].unregisterMetrics()
g.Rules[i].close()
g.Rules[i] = nil
continue
}
@@ -270,7 +257,6 @@ func (g *Group) updateWith(newGroup *Group) error {
}
// add the rest of rules from registry
for _, nr := range rulesRegistry {
nr.registerMetrics(g)
newRules = append(newRules, nr)
}
// note that g.Interval is not updated here
@@ -309,7 +295,13 @@ func (g *Group) Close() {
g.InterruptEval()
<-g.finishedCh
g.metrics.close()
g.metrics.iterationDuration.Unregister()
g.metrics.iterationTotal.Unregister()
g.metrics.iterationMissed.Unregister()
g.metrics.iterationInterval.Unregister()
for _, rule := range g.Rules {
rule.close()
}
}
// SkipRandSleepOnGroupStart will skip random sleep delay in group first evaluation
@@ -319,8 +311,6 @@ var SkipRandSleepOnGroupStart bool
func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw remotewrite.RWClient, rr datasource.QuerierBuilder) {
defer func() { close(g.finishedCh) }()
g.metrics.start()
evalTS := time.Now()
// sleep random duration to spread group rules evaluation
// over time in order to reduce load on datasource.

View File

@@ -10,7 +10,6 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metrics"
"gopkg.in/yaml.v2"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
@@ -42,7 +41,6 @@ func TestUpdateWith(t *testing.T) {
g := &Group{
Name: "test",
}
g.metrics = newGroupMetrics(g)
qb := &datasource.FakeQuerier{}
for _, r := range currentRules {
r.ID = config.HashRule(r)
@@ -52,7 +50,6 @@ func TestUpdateWith(t *testing.T) {
ng := &Group{
Name: "test",
}
ng.metrics = newGroupMetrics(ng)
for _, r := range newRules {
r.ID = config.HashRule(r)
ng.Rules = append(ng.Rules, ng.newRule(qb, r))
@@ -198,7 +195,6 @@ func TestUpdateDuringRandSleep(t *testing.T) {
Interval: 100 * time.Hour,
updateCh: make(chan *Group),
}
g.metrics = newGroupMetrics(g)
go g.Start(context.Background(), nil, nil, nil)
rule1 := AlertingRule{
@@ -213,7 +209,6 @@ func TestUpdateDuringRandSleep(t *testing.T) {
&rule1,
},
}
g1.metrics = newGroupMetrics(g1)
g.updateCh <- g1
time.Sleep(10 * time.Millisecond)
g.mu.RLock()
@@ -223,9 +218,8 @@ func TestUpdateDuringRandSleep(t *testing.T) {
g.mu.RUnlock()
rule2 := AlertingRule{
RuleID: 1,
Name: "jobDown",
Expr: "up{job=\"vmagent\"}==0",
Name: "jobDown",
Expr: "up{job=\"vmagent\"}==0",
Labels: map[string]string{
"foo": "bar",
"baz": "qux",
@@ -233,32 +227,17 @@ func TestUpdateDuringRandSleep(t *testing.T) {
}
g2 := &Group{
Rules: []Rule{
&rule1,
&rule2,
},
}
g2.metrics = newGroupMetrics(g2)
g.updateCh <- g2
time.Sleep(10 * time.Millisecond)
g.mu.RLock()
if len(g.Rules) != 2 {
t.Fatalf("expected to have updated rules")
}
if len(g.Rules[1].(*AlertingRule).Labels) != 2 {
if len(g.Rules[0].(*AlertingRule).Labels) != 2 {
t.Fatalf("expected to have updated labels")
}
g.mu.RUnlock()
metricsAfter := metrics.GetDefaultSet().ListMetricNames()
metricsRegistry := make(map[string]struct{}, len(metricsAfter))
for _, m := range metricsAfter {
if _, ok := metricsRegistry[m]; ok {
t.Fatalf("duplicate metric name %q", m)
}
metricsRegistry[m] = struct{}{}
}
g.Close()
}

View File

@@ -6,8 +6,6 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
@@ -47,28 +45,6 @@ type recordingRuleMetrics struct {
samples *utils.Gauge
}
func newRecordingRuleMetrics(set *metrics.Set, rr *RecordingRule) *recordingRuleMetrics {
rmr := &recordingRuleMetrics{}
labels := fmt.Sprintf(`recording=%q, group=%q, file=%q, id="%d"`, rr.Name, rr.GroupName, rr.File, rr.ID())
rmr.errors = utils.NewCounter(set, fmt.Sprintf(`vmalert_recording_rules_errors_total{%s}`, labels))
rmr.samples = utils.NewGauge(set, fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := rr.state.getLast()
return float64(e.Samples)
})
return rmr
}
func (m *recordingRuleMetrics) close() {
if m == nil {
return
}
m.errors.Unregister()
m.samples.Unregister()
}
// String implements Stringer interface
func (rr *RecordingRule) String() string {
return rr.Name
@@ -91,6 +67,7 @@ func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
GroupID: group.ID(),
GroupName: group.Name,
File: group.File,
metrics: &recordingRuleMetrics{},
q: qb.BuildWithParams(datasource.QuerierParams{
DataSourceType: group.Type.String(),
ApplyIntervalAsTimeFilter: setIntervalAsTimeFilter(group.Type.String(), cfg.Expr),
@@ -110,18 +87,21 @@ func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
rr.state = &ruleState{
entries: make([]StateEntry, entrySize),
}
rr.metrics = newRecordingRuleMetrics(group.metrics.set, rr)
labels := fmt.Sprintf(`recording=%q, group=%q, file=%q, id="%d"`, rr.Name, group.Name, group.File, rr.ID())
rr.metrics.errors = utils.GetOrCreateCounter(fmt.Sprintf(`vmalert_recording_rules_errors_total{%s}`, labels))
rr.metrics.samples = utils.GetOrCreateGauge(fmt.Sprintf(`vmalert_recording_rules_last_evaluation_samples{%s}`, labels),
func() float64 {
e := rr.state.getLast()
return float64(e.Samples)
})
return rr
}
func (rr *RecordingRule) registerMetrics(g *Group) {
rr.metrics = newRecordingRuleMetrics(g.metrics.set, rr)
}
// close unregisters rule metrics
func (rr *RecordingRule) unregisterMetrics() {
rr.metrics.close()
func (rr *RecordingRule) close() {
rr.metrics.errors.Unregister()
rr.metrics.samples.Unregister()
}
// execRange executes recording rule on the given time range similarly to Exec.

View File

@@ -7,9 +7,8 @@ import (
"testing"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/utils"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
)
@@ -307,12 +306,15 @@ func TestRecordingRuleLimit_Failure(t *testing.T) {
fq := &datasource.FakeQuerier{}
fq.Add(testMetrics...)
rule := &RecordingRule{Name: "job:foo",
state: &ruleState{entries: make([]StateEntry, 10)},
Labels: map[string]string{
"source": "test_limit",
},
metrics: getTestRecordingRuleMetrics(),
metrics: &recordingRuleMetrics{
errors: utils.GetOrCreateCounter(`vmalert_recording_rules_errors_total{alertname="job:foo"}`),
},
}
rule.q = fq
@@ -342,12 +344,15 @@ func TestRecordingRuleLimit_Success(t *testing.T) {
fq := &datasource.FakeQuerier{}
fq.Add(testMetrics...)
rule := &RecordingRule{Name: "job:foo",
state: &ruleState{entries: make([]StateEntry, 10)},
Labels: map[string]string{
"source": "test_limit",
},
metrics: getTestRecordingRuleMetrics(),
metrics: &recordingRuleMetrics{
errors: utils.GetOrCreateCounter(`vmalert_recording_rules_errors_total{alertname="job:foo"}`),
},
}
rule.q = fq
@@ -361,19 +366,16 @@ func TestRecordingRuleLimit_Success(t *testing.T) {
f(-1)
}
func getTestRecordingRuleMetrics() *recordingRuleMetrics {
m := newRecordingRuleMetrics(metrics.NewSet(), &RecordingRule{})
return m
}
func TestRecordingRuleExec_Negative(t *testing.T) {
rr := &RecordingRule{
Name: "job:foo",
Labels: map[string]string{
"job": "test",
},
state: &ruleState{entries: make([]StateEntry, 10)},
metrics: getTestRecordingRuleMetrics(),
state: &ruleState{entries: make([]StateEntry, 10)},
metrics: &recordingRuleMetrics{
errors: utils.GetOrCreateCounter(`vmalert_recording_rules_errors_total{alertname="job:foo"}`),
},
}
fq := &datasource.FakeQuerier{}
expErr := "connection reset by peer"

View File

@@ -27,10 +27,9 @@ type Rule interface {
// updateWith performs modification of current Rule
// with fields of the given Rule.
updateWith(Rule) error
// unregister Rule metrics
unregisterMetrics()
// register Rule metrics with the given group
registerMetrics(g *Group)
// close performs the shutdown procedures for rule
// such as metrics unregister
close()
}
var errDuplicate = errors.New("result contains metrics with the same labelset during evaluation. See https://docs.victoriametrics.com/vmalert/#series-with-the-same-labelset for details")

View File

@@ -33,7 +33,7 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/formatutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
// go template execution fails when it's tree is empty
@@ -259,7 +259,7 @@ func templateFuncs() textTpl.FuncMap {
// parseDuration parses a duration string such as "1h" into the number of seconds it represents
"parseDuration": func(s string) (float64, error) {
d, err := timeutil.ParseDuration(s)
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}
@@ -268,7 +268,7 @@ func templateFuncs() textTpl.FuncMap {
// same with parseDuration but returns a time.Duration
"parseDurationTime": func(s string) (time.Duration, error) {
d, err := timeutil.ParseDuration(s)
d, err := promutils.ParseDuration(s)
if err != nil {
return 0, err
}

View File

@@ -4,13 +4,11 @@ import "github.com/VictoriaMetrics/metrics"
type namedMetric struct {
Name string
set *metrics.Set
}
// Unregister removes the metric by name from default registry
func (nm namedMetric) Unregister() {
nm.set.UnregisterMetric(nm.Name)
metrics.UnregisterMetric(nm.Name)
}
// Gauge is a metrics.Gauge with Name
@@ -19,11 +17,11 @@ type Gauge struct {
*metrics.Gauge
}
// NewGauge creates a new Gauge with the given name
func NewGauge(set *metrics.Set, name string, f func() float64) *Gauge {
// GetOrCreateGauge creates a new Gauge with the given name
func GetOrCreateGauge(name string, f func() float64) *Gauge {
return &Gauge{
namedMetric: namedMetric{Name: name, set: set},
Gauge: set.NewGauge(name, f),
namedMetric: namedMetric{Name: name},
Gauge: metrics.GetOrCreateGauge(name, f),
}
}
@@ -33,11 +31,11 @@ type Counter struct {
*metrics.Counter
}
// NewCounter creates a new Counter with the given name
func NewCounter(set *metrics.Set, name string) *Counter {
// GetOrCreateCounter creates a new Counter with the given name
func GetOrCreateCounter(name string) *Counter {
return &Counter{
namedMetric: namedMetric{Name: name, set: set},
Counter: set.NewCounter(name),
namedMetric: namedMetric{Name: name},
Counter: metrics.GetOrCreateCounter(name),
}
}
@@ -47,10 +45,10 @@ type Summary struct {
*metrics.Summary
}
// NewSummary creates a new Summary with the given name
func NewSummary(set *metrics.Set, name string) *Summary {
// GetOrCreateSummary creates a new Summary with the given name
func GetOrCreateSummary(name string) *Summary {
return &Summary{
namedMetric: namedMetric{Name: name, set: set},
Summary: set.NewSummary(name),
namedMetric: namedMetric{Name: name},
Summary: metrics.GetOrCreateSummary(name),
}
}

View File

@@ -21,18 +21,14 @@ func TestHandler(t *testing.T) {
fq.Add(datasource.Metric{
Values: []float64{1}, Timestamps: []int64{0},
})
g := rule.NewGroup(config.Group{
g := &rule.Group{
Name: "group",
File: "rules.yaml",
Concurrency: 1,
Rules: []config.Rule{
{ID: 0, Alert: "alert"},
{ID: 1, Record: "record"},
},
}, fq, 1*time.Minute, nil)
ar := g.Rules[0].(*rule.AlertingRule)
rr := g.Rules[1].(*rule.RecordingRule)
}
ar := rule.NewAlertingRule(fq, g, config.Rule{ID: 0, Alert: "alert"})
rr := rule.NewRecordingRule(fq, g, config.Rule{ID: 1, Record: "record"})
g.Rules = []rule.Rule{ar, rr}
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
m := &manager{groups: map[uint64]*rule.Group{
@@ -166,7 +162,7 @@ func TestHandler(t *testing.T) {
gotRuleWithUpdates := apiRuleWithUpdates{}
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRuleWithUpdates, 200)
if len(gotRuleWithUpdates.StateUpdates) < 1 {
if gotRuleWithUpdates.StateUpdates == nil || len(gotRuleWithUpdates.StateUpdates) < 1 {
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
}
})

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"reflect"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
@@ -16,22 +15,20 @@ func TestRecordingToApi(t *testing.T) {
fq.Add(datasource.Metric{
Values: []float64{1}, Timestamps: []int64{0},
})
entriesLimit := 44
g := rule.NewGroup(config.Group{
g := &rule.Group{
Name: "group",
File: "rules.yaml",
Concurrency: 1,
Rules: []config.Rule{
{
ID: 1248,
Record: "record_name",
Expr: "up",
Labels: map[string]string{"label": "value"},
UpdateEntriesLimit: &entriesLimit,
},
},
}, fq, 1*time.Minute, nil)
rr := g.Rules[0].(*rule.RecordingRule)
}
entriesLimit := 44
rr := rule.NewRecordingRule(fq, g, config.Rule{
ID: 1248,
Record: "record_name",
Expr: "up",
Labels: map[string]string{"label": "value"},
UpdateEntriesLimit: &entriesLimit,
})
expectedRes := apiRule{
Name: "record_name",

View File

@@ -98,7 +98,6 @@ func TestCreateTargetURLSuccess(t *testing.T) {
up, hc := ui.getURLPrefixAndHeaders(u, u.Host, nil)
if up == nil {
t.Fatalf("cannot match available backend: %s", err)
return
}
bu := up.getBackendURL()
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)
@@ -310,7 +309,6 @@ func TestUserInfoGetBackendURL_SRV(t *testing.T) {
up, _ := ui.getURLPrefixAndHeaders(u, u.Host, nil)
if up == nil {
t.Fatalf("cannot match available backend: %s", err)
return
}
bu := up.getBackendURL()
target := mergeURLs(bu.url, u, up.dropSrcPathPrefixParts)

View File

@@ -344,7 +344,7 @@ var (
},
&cli.BoolFlag{
Name: influxSkipDatabaseLabel,
Usage: "Whether to skip adding the label 'db' to timeseries.",
Usage: "Wether to skip adding the label 'db' to timeseries.",
Value: false,
},
&cli.BoolFlag{

View File

@@ -144,9 +144,9 @@ func timeFilter(start, end string) string {
// by checking available (non-empty) tags, fields and measurements
// which unique combination represents all possible
// time series existing in database.
// Explore is required to reduce the load on influx
// The explore required to reduce the load on influx
// by querying field of the exact time series at once,
// instead of fetching all the values over and over.
// instead of fetching all of the values over and over.
//
// May contain non-existing time series.
func (c *Client) Explore() ([]*Series, error) {
@@ -340,7 +340,10 @@ func (c *Client) fieldsByMeasurement() (map[string][]string, error) {
}
func (c *Client) getSeries() ([]*Series, error) {
com := c.getSeriesCommand()
com := "show series"
if c.filterSeries != "" {
com = fmt.Sprintf("%s %s", com, c.filterSeries)
}
q := influx.Query{
Command: com,
Database: c.database,
@@ -386,21 +389,6 @@ func (c *Client) getSeries() ([]*Series, error) {
return result, nil
}
func (c *Client) getSeriesCommand() string {
com := "show series"
if c.filterSeries != "" {
com = fmt.Sprintf("%s %s", com, c.filterSeries)
}
if c.filterTime != "" {
joinStatement := " where "
if strings.Contains(strings.ToLower(com), joinStatement) {
joinStatement = " AND "
}
com = fmt.Sprintf("%s%s%s", com, joinStatement, c.filterTime)
}
return com
}
// getMeasurementTags get the tags for each measurement.
// tags are placed in a map without values (similar to a set) for quick lookups:
// {"measurement1": {"tag1", "tag2"}, "measurement2": {"tag3", "tag4"}}

View File

@@ -103,26 +103,3 @@ func TestTimeFilter(t *testing.T) {
// both start and end filters
f("2020-01-01T20:07:00Z", "2020-01-01T21:07:00Z", "time >= '2020-01-01T20:07:00Z' and time <= '2020-01-01T21:07:00Z'")
}
func TestGetSeriesCommand(t *testing.T) {
f := func(filterSeries, filterTime, expCommand string) {
t.Helper()
c := &Client{
filterTime: filterTime,
filterSeries: filterSeries,
}
gotCommand := c.getSeriesCommand()
if gotCommand != expCommand {
t.Fatalf("unexpected command\ngot\n%s\nwant\n%s", gotCommand, expCommand)
}
}
f("", "", "show series")
f("from cpu", "", "show series from cpu")
f("from cpu where arch='x86'", "", "show series from cpu where arch='x86'")
f("", "time >= '2020-01-01T20:07:00Z'", "show series where time >= '2020-01-01T20:07:00Z'")
f("from cpu", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where time >= '2020-01-01T20:07:00Z'")
f("from cpu where arch='x86'", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where arch='x86' AND time >= '2020-01-01T20:07:00Z'")
f("from cpu where arch='x86' AND hostname='host_2753'", "time >= '2020-01-01T20:07:00Z'", "show series from cpu where arch='x86' AND hostname='host_2753' AND time >= '2020-01-01T20:07:00Z'")
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)
const (
@@ -16,7 +16,7 @@ const (
// ParseTime parses time in s string and returns time.Time object
// if parse correctly or error if not
func ParseTime(s string) (time.Time, error) {
msecs, err := timeutil.ParseTimeMsec(s)
msecs, err := promutils.ParseTimeMsec(s)
if err != nil {
return time.Time{}, fmt.Errorf("cannot parse %s: %w", s, err)
}

View File

@@ -36,9 +36,6 @@ var (
"See https://docs.victoriametrics.com/stream-aggregation/#ignoring-old-samples")
streamAggrIgnoreFirstIntervals = flag.Int("streamAggr.ignoreFirstIntervals", 0, "Number of aggregation intervals to skip after the start. Increase this value if you observe incorrect aggregation results after restarts. It could be caused by receiving unordered delayed data from clients pushing data into the database. "+
"See https://docs.victoriametrics.com/stream-aggregation/#ignore-aggregation-intervals-on-start")
streamAggrEnableWindows = flag.Bool("streamAggr.enableWindows", false, "Enables aggregation within fixed windows for all aggregators. "+
"This allows to get more precise results, but impacts resource usage as it requires twice more memory to store two states. "+
"See https://docs.victoriametrics.com/stream-aggregation/#aggregation-windows.")
)
var (
@@ -65,7 +62,6 @@ func CheckStreamAggrConfig() error {
DropInputLabels: *streamAggrDropInputLabels,
IgnoreOldSamples: *streamAggrIgnoreOldSamples,
IgnoreFirstIntervals: *streamAggrIgnoreFirstIntervals,
EnableWindows: *streamAggrEnableWindows,
}
sas, err := streamaggr.LoadFromFile(*streamAggrConfig, pushNoop, opts, "global")
if err != nil {
@@ -82,7 +78,7 @@ func InitStreamAggr() {
saCfgReloaderStopCh = make(chan struct{})
if *streamAggrConfig == "" {
if *streamAggrDedupInterval > 0 {
deduplicator = streamaggr.NewDeduplicator(pushAggregateSeries, *streamAggrEnableWindows, *streamAggrDedupInterval, *streamAggrDropInputLabels, "global")
deduplicator = streamaggr.NewDeduplicator(pushAggregateSeries, *streamAggrDedupInterval, *streamAggrDropInputLabels, "global")
}
return
}

View File

@@ -2,7 +2,6 @@ package prompush
import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompbmarshal"
"github.com/VictoriaMetrics/metrics"
@@ -50,7 +49,6 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
}
ctx.Reset(rowsLen)
rowsTotal := 0
hasRelabeling := relabel.HasRelabeling()
for i := range tss {
ts := &tss[i]
rowsTotal += len(ts.Samples)
@@ -59,7 +57,7 @@ func push(ctx *common.InsertCtx, tss []prompbmarshal.TimeSeries) {
label := &ts.Labels[j]
ctx.AddLabel(label.Name, label.Value)
}
if !ctx.TryPrepareLabels(hasRelabeling) {
if !ctx.TryPrepareLabels(false) {
continue
}
var metricNameRaw []byte

View File

@@ -791,7 +791,7 @@ func LabelNames(qt *querytracer.Tracer, sq *storage.SearchQuery, maxLabelNames i
if err != nil {
return nil, err
}
labels, err := vmstorage.SearchLabelNames(qt, tfss, tr, maxLabelNames, sq.MaxMetrics, deadline.Deadline())
labels, err := vmstorage.SearchLabelNamesWithFiltersOnTimeRange(qt, tfss, tr, maxLabelNames, sq.MaxMetrics, deadline.Deadline())
if err != nil {
return nil, fmt.Errorf("error during labels search on time range: %w", err)
}
@@ -864,7 +864,7 @@ func LabelValues(qt *querytracer.Tracer, labelName string, sq *storage.SearchQue
if err != nil {
return nil, err
}
labelValues, err := vmstorage.SearchLabelValues(qt, labelName, tfss, tr, maxLabelValues, sq.MaxMetrics, deadline.Deadline())
labelValues, err := vmstorage.SearchLabelValuesWithFiltersOnTimeRange(qt, labelName, tfss, tr, maxLabelValues, sq.MaxMetrics, deadline.Deadline())
if err != nil {
return nil, fmt.Errorf("error during label values search on time range for labelName=%q: %w", labelName, err)
}

View File

@@ -39,7 +39,7 @@ var (
"It can be overridden on per-query basis via latency_offset arg. "+
"Too small value can result in incomplete last points for query results")
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -search.lookback-delta from Prometheus. "+
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
maxStalenessInterval = flag.Duration("search.maxStalenessInterval", 0, "The maximum interval for staleness calculations. "+

View File

@@ -373,17 +373,9 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
func(values []float64, timestamps []int64), []*rollupConfig, error) {
preFunc := func(_ []float64, _ []int64) {}
funcName = strings.ToLower(funcName)
// window > lookbackDelta could result in negative delta.
// See issue: https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8342
stalenessInterval := lookbackDelta
if stalenessInterval != 0 && stalenessInterval < window {
stalenessInterval = window
}
if rollupFuncsRemoveCounterResets[funcName] {
preFunc = func(values []float64, timestamps []int64) {
removeCounterResets(values, timestamps, stalenessInterval)
removeCounterResets(values, timestamps, lookbackDelta)
}
}
samplesScannedPerCall := rollupFuncsSamplesScannedPerCall[funcName]
@@ -495,7 +487,7 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
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, stalenessInterval)
removeCounterResets(values, timestamps, lookbackDelta)
}
}
rf := rollupAggrFuncs[aggrFuncName]

View File

@@ -0,0 +1,13 @@
{
"files": {
"main.css": "./static/css/main.7fa18e1b.css",
"main.js": "./static/js/main.ba08300f.js",
"static/js/685.f772060c.chunk.js": "./static/js/685.f772060c.chunk.js",
"static/media/MetricsQL.md": "./static/media/MetricsQL.a00044c91d9781cf8557.md",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.7fa18e1b.css",
"static/js/main.ba08300f.js"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.uplot,.uplot *,.uplot *:before,.uplot *:after{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5;width:min-content}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{position:relative;-webkit-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;position:relative;width:100%;height:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:#00000012;position:absolute;pointer-events:none}.u-cursor-x,.u-cursor-y{position:absolute;left:0;top:0;pointer-events:none;will-change:transform}.u-hz .u-cursor-x,.u-vt .u-cursor-y{height:100%;border-right:1px dashed #607D8B}.u-hz .u-cursor-y,.u-vt .u-cursor-x{width:100%;border-bottom:1px dashed #607D8B}.u-cursor-pt{position:absolute;top:0;left:0;border-radius:50%;border:0 solid;pointer-events:none;will-change:transform;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +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"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json"/>
<!--
Notice the use of in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>vmui</title>
<script src="./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 type="module" crossorigin src="./assets/index-DzehQsnZ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DojlIpLz.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-Cqbobgy7.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
<!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>

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

@@ -0,0 +1,38 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @remix-run/router v1.19.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router DOM v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

View File

@@ -71,11 +71,6 @@ var (
"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")
disablePerDayIndex = flag.Bool("disablePerDayIndex", false, "Disable per-day index and use global index for all searches. "+
"This may improve performance and decrease disk space usage for the use cases with fixed set of timeseries scattered across a "+
"big time range (for example, when loading years of historical data). "+
"See https://docs.victoriametrics.com/single-server-victoriametrics/#index-tuning")
)
// CheckTimeRange returns true if the given tr is denied for querying.
@@ -115,14 +110,7 @@ func Init(resetCacheIfNeeded func(mrs []storage.MetricRow)) {
logger.Infof("opening storage at %q with -retentionPeriod=%s", *DataPath, retentionPeriod)
startTime := time.Now()
WG = syncwg.WaitGroup{}
opts := storage.OpenOptions{
Retention: retentionPeriod.Duration(),
MaxHourlySeries: *maxHourlySeries,
MaxDailySeries: *maxDailySeries,
DisablePerDayIndex: *disablePerDayIndex,
}
strg := storage.MustOpenStorage(*DataPath, opts)
strg := storage.MustOpenStorage(*DataPath, retentionPeriod.Duration(), *maxHourlySeries, *maxDailySeries)
Storage = strg
initStaleSnapshotsRemover(strg)
@@ -201,19 +189,20 @@ func SearchMetricNames(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr st
return metricNames, err
}
// SearchLabelNames searches for tag keys matching the given tfss on tr.
func SearchLabelNames(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr storage.TimeRange, maxTagKeys, maxMetrics int, deadline uint64) ([]string, error) {
// SearchLabelNamesWithFiltersOnTimeRange searches for tag keys matching the given tfss on tr.
func SearchLabelNamesWithFiltersOnTimeRange(qt *querytracer.Tracer, tfss []*storage.TagFilters, tr storage.TimeRange, maxTagKeys, maxMetrics int, deadline uint64) ([]string, error) {
WG.Add(1)
labelNames, err := Storage.SearchLabelNames(qt, tfss, tr, maxTagKeys, maxMetrics, deadline)
labelNames, err := Storage.SearchLabelNamesWithFiltersOnTimeRange(qt, tfss, tr, maxTagKeys, maxMetrics, deadline)
WG.Done()
return labelNames, err
}
// SearchLabelValues searches for label values for the given labelName, tfss and
// tr.
func SearchLabelValues(qt *querytracer.Tracer, labelName string, tfss []*storage.TagFilters, tr storage.TimeRange, maxLabelValues, maxMetrics int, deadline uint64) ([]string, error) {
// SearchLabelValuesWithFiltersOnTimeRange searches for label values for the given labelName, tfss and tr.
func SearchLabelValuesWithFiltersOnTimeRange(qt *querytracer.Tracer, labelName string, tfss []*storage.TagFilters,
tr storage.TimeRange, maxLabelValues, maxMetrics int, deadline uint64,
) ([]string, error) {
WG.Add(1)
labelValues, err := Storage.SearchLabelValues(qt, labelName, tfss, tr, maxLabelValues, maxMetrics, deadline)
labelValues, err := Storage.SearchLabelValuesWithFiltersOnTimeRange(qt, labelName, tfss, tr, maxLabelValues, maxMetrics, deadline)
WG.Done()
return labelValues, err
}

View File

@@ -1,4 +1,4 @@
FROM golang:1.24.0 AS build-web-stage
FROM golang:1.23.6 AS build-web-stage
COPY build /build
WORKDIR /build
@@ -6,7 +6,7 @@ COPY web/ /build/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
FROM alpine:3.21.3
FROM alpine:3.21.2
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,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

@@ -1,18 +1,22 @@
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
import { gradMetal16 } from "../../../../utils/uplot";
import { SeriesItem, LegendItemType } from "../../../../types";
import "./style.scss";
import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
interface LegendHeatmapProps {
min: number
max: number
legendValue: ChartTooltipProps | null,
series: SeriesItem[]
}
const LegendHeatmap: FC<LegendHeatmapProps> = ({
min,
max,
legendValue,
series
}) => {
const [percent, setPercent] = useState(0);
@@ -50,6 +54,13 @@ const LegendHeatmap: FC<LegendHeatmapProps> = ({
<div className="vm-legend-heatmap__value">{minFormat}</div>
<div className="vm-legend-heatmap__value">{maxFormat}</div>
</div>
{series[1] && (
<LegendItem
key={series[1]?.label}
legend={series[1] as LegendItemType}
isHeatmap
/>
)}
</div>
);
};

View File

@@ -1,15 +1,8 @@
import React, { FC } from "preact/compat";
import React, { FC, useMemo } from "preact/compat";
import { LegendItemType } from "../../../../types";
import LegendItem from "./LegendItem/LegendItem";
import Accordion from "../../../Main/Accordion/Accordion";
import "./style.scss";
import LegendGroup from "./LegendGroup";
import { useLegendGroup } from "./hooks/useLegendGroup";
import { useGroupSeries } from "./hooks/useGroupSeries";
export type QueryGroup = {
group: number | string;
items: LegendItemType[]
}
interface LegendProps {
labels: LegendItemType[];
@@ -19,34 +12,42 @@ interface LegendProps {
}
const Legend: FC<LegendProps> = ({ labels, query, isAnomalyView, onChange }) => {
const { groupByLabel } = useLegendGroup();
const groupSeries = useGroupSeries({ labels, query, groupByLabel });
const groups = useMemo(() => {
return Array.from(new Set(labels.map(l => l.group)));
}, [labels]);
const showQueryNum = groups.length > 1;
return <>
<div className="vm-legend">
<div>
{groupSeries.map(({ group, items }) => (
<div
className="vm-legend-group"
key={group}
{groups.map((group) => (
<div
className="vm-legend-group"
key={group}
>
<Accordion
defaultExpanded={true}
title={(
<div className="vm-legend-group-title">
{showQueryNum && (
<span className="vm-legend-group-title__count">Query {group}: </span>
)}
<span className="vm-legend-group-title__query">{query[group - 1]}</span>
</div>
)}
>
<Accordion
defaultExpanded={true}
title={(
<div className="vm-legend-group-title">
Group by{groupByLabel ? "" : " query"}: <b>{group}</b>
</div>
<div>
{labels.filter(l => l.group === group).sort((x, y) => (y.median || 0) - (x.median || 0)).map((legendItem: LegendItemType) =>
<LegendItem
key={legendItem.label}
legend={legendItem}
isAnomalyView={isAnomalyView}
onChange={onChange}
/>
)}
>
<LegendGroup
labels={items}
isAnomalyView={isAnomalyView}
onChange={onChange}
/>
</Accordion>
</div>
))}
</div>
</div>
</Accordion>
</div>
))}
</div>
</>;
};

View File

@@ -1,104 +0,0 @@
import React, { FC, useMemo } from "preact/compat";
import Switch from "../../../../Main/Switch/Switch";
import { LegendDisplayType, useLegendView } from "../hooks/useLegendView";
import { useHideDuplicateFields } from "../hooks/useHideDuplicateFields";
import { useShowStats } from "../hooks/useShowStats";
import TextField from "../../../../Main/TextField/TextField";
import { useLegendFormat } from "../hooks/useLegendFormat";
import { WITHOUT_GROUPING } from "../../../../../constants/logs";
import Select from "../../../../Main/Select/Select";
import { useLegendGroup } from "../hooks/useLegendGroup";
import "./style.scss";
import { MetricResult } from "../../../../../api/types";
type Props = {
data: MetricResult[]
}
const LegendConfigs: FC<Props> = ({ data }) => {
const { isTableView, onChange: onChangeView } = useLegendView();
const { hideDuplicates, onChange: onChangeDuplicates } = useHideDuplicateFields();
const { hideStats, onChange: onChangeStats } = useShowStats();
const { format, onChange: onChangeFormat, onApply: onApplyFormat } = useLegendFormat();
const { groupByLabel, onChange: onChangeGroup } = useLegendGroup();
const uniqueFields = useMemo(() => {
const fields = data.flatMap(d => Object.keys(d.metric));
return Array.from(new Set(fields));
}, [data]);
const handleChangeView = (val: boolean) => {
const value = val ? LegendDisplayType.table : LegendDisplayType.lines;
onChangeView(value);
};
return (
<>
<div className="vm-legend-configs-item vm-legend-configs-item_switch">
<span className="vm-legend-configs-item__label">Table View</span>
<Switch
label={isTableView ? "Enabled" : "Disabled"}
value={isTableView}
onChange={handleChangeView}
/>
<span className="vm-legend-configs-item__info">
Switches between table and lines view.
</span>
</div>
<div className="vm-legend-configs-item vm-legend-configs-item_switch">
<span className="vm-legend-configs-item__label">Common Labels</span>
<Switch
label={hideDuplicates ? "Hide" : "Show"}
value={!hideDuplicates}
onChange={onChangeDuplicates}
/>
<span className="vm-legend-configs-item__info">
Shows or hides labels that are the same for all series.
</span>
</div>
<div className="vm-legend-configs-item vm-legend-configs-item_switch">
<span className="vm-legend-configs-item__label">Statistics</span>
<Switch
label={hideStats ? "Hide" : "Show"}
value={!hideStats}
onChange={onChangeStats}
/>
<span className="vm-legend-configs-item__info">
Displays min, median, and max values.
</span>
</div>
<div className="vm-legend-configs-item">
<TextField
label="Custom Legend Format"
placeholder={"{{label_name}}"}
value={format}
onChange={onChangeFormat}
onBlur={onApplyFormat}
onEnter={onApplyFormat}
/>
<span className="vm-legend-configs-item__info vm-legend-configs-item__info_input">
Customize legend labels with text and &#123;&#123;label_name&#125;&#125; placeholders.
</span>
</div>
<div className="vm-legend-configs-item">
<Select
label="Group Legend By"
value={groupByLabel}
list={[WITHOUT_GROUPING, ...uniqueFields]}
placeholder={WITHOUT_GROUPING}
onChange={onChangeGroup}
searchable
/>
<span className="vm-legend-configs-item__info">
Choose a label to group the legend. By default, legends are grouped by query.
</span>
</div>
</>
);
};
export default LegendConfigs;

View File

@@ -1,26 +0,0 @@
@use "src/styles/variables" as *;
.vm-legend-configs {
&-item {
display: grid;
align-items: center;
justify-content: stretch;
margin-top: calc($padding-global/2);
&_switch {
grid-template-columns: 1fr 100px;
margin-top: 0;
}
&__info {
margin-top: calc($padding-global/2);
font-size: $font-size-small;
color: $color-text-secondary;
line-height: 130%;
&_input {
margin-top: 0;
}
}
}
}

View File

@@ -1,44 +0,0 @@
import React, { FC, useMemo } from "react";
import { LegendItemType } from "../../../../types";
import { useLegendView } from "./hooks/useLegendView";
import LegendLines from "./LegendViews/LegendLines";
import LegendTable from "./LegendViews/LegendTable";
import { useHideDuplicateFields } from "./hooks/useHideDuplicateFields";
export type LegendProps = {
labels: LegendItemType[];
isAnomalyView?: boolean;
duplicateFields?: string[];
onChange: (item: LegendItemType, metaKey: boolean) => void;
}
const LegendGroup: FC<LegendProps> = ({ labels, isAnomalyView, onChange }) => {
const { isTableView } = useLegendView();
const { duplicateFields } = useHideDuplicateFields(labels);
const sortedLabels = useMemo(() => {
return labels.sort((x, y) => (y.median || 0) - (x.median || 0));
}, [labels]);
if (isTableView) {
return (
<LegendTable
labels={sortedLabels}
isAnomalyView={isAnomalyView}
duplicateFields={duplicateFields}
onChange={onChange}
/>
);
}
return (
<LegendLines
labels={sortedLabels}
isAnomalyView={isAnomalyView}
duplicateFields={duplicateFields}
onChange={onChange}
/>
);
};
export default LegendGroup;

View File

@@ -6,41 +6,36 @@ import classNames from "classnames";
import { getFreeFields } from "./helpers";
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
import { STATS_ORDER } from "../../../../../constants/graph";
import { useShowStats } from "../hooks/useShowStats";
import { useLegendFormat } from "../hooks/useLegendFormat";
import { getLabelAlias } from "../../../../../utils/metric";
interface LegendItemProps {
legend: LegendItemType;
onChange?: (item: LegendItemType, metaKey: boolean) => void;
isHeatmap?: boolean;
isAnomalyView?: boolean;
duplicateFields?: string[];
}
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, isAnomalyView }) => {
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap, isAnomalyView }) => {
const copyToClipboard = useCopyToClipboard();
const { hideStats } = useShowStats();
const { format } = useLegendFormat();
const formattedLabel = getLabelAlias(legend.freeFormFields, format);
const freeFormFields = useMemo(() => {
const result = getFreeFields(legend);
return duplicateFields?.length
? result.filter(f => !duplicateFields.includes(f.key))
: result;
}, [legend, duplicateFields]);
return isHeatmap ? result.filter(f => f.key !== "vmrange") : result;
}, [legend, isHeatmap]);
const statsFormatted = legend.statsFormatted;
const showStats = Object.values(statsFormatted).some(v => v);
const handleClickFreeField = async (val: string) => {
await copyToClipboard(val, `${val} has been copied`);
};
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
onChange && onChange(legend, e.ctrlKey || e.metaKey);
};
const createHandlerCopy = (freeField: string) => async (e: MouseEvent<HTMLDivElement>) => {
const createHandlerCopy = (freeField: string) => (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
await copyToClipboard(freeField, `${freeField} has been copied`);
handleClickFreeField(freeField);
};
return (
@@ -48,11 +43,12 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, is
className={classNames({
"vm-legend-item": true,
"vm-legend-row": true,
"vm-legend-item_hide": !legend.checked,
"vm-legend-item_hide": !legend.checked && !isHeatmap,
"vm-legend-item_static": isHeatmap,
})}
onClick={createHandlerClick(legend)}
>
{!isAnomalyView && (
{!isAnomalyView && !isHeatmap && (
<div
className="vm-legend-item__marker"
style={{ backgroundColor: legend.color }}
@@ -60,12 +56,10 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, is
)}
<div className="vm-legend-item-info">
<span className="vm-legend-item-info__label">
{legend.hasAlias && legend.label}
{!legend.hasAlias && format && formattedLabel}
{!legend.hasAlias && !format && (
{legend.hasAlias ? legend.label : (
<>
{legend.freeFormFields["__name__"]}
{!!freeFormFields.length && <> &#123;</>}
{!!freeFormFields.length && <>&#123;</>}
{freeFormFields.map((f, i) => (
<span
className="vm-legend-item-info__free-fields"
@@ -81,7 +75,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, duplicateFields, is
)}
</span>
</div>
{!hideStats && showStats && (
{!isHeatmap && showStats && (
<div className="vm-legend-item-stats">
{STATS_ORDER.map((key, i) => (
<div

View File

@@ -18,7 +18,18 @@
&_hide {
text-decoration: line-through;
opacity: 0.2;
opacity: 0.5;
}
&_static {
grid-template-columns: 1fr;
margin: 0;
padding: 0;
cursor: default;
&:hover {
background-color: $color-background-block;
}
}
&__marker {

View File

@@ -1,22 +0,0 @@
import React, { FC } from "preact/compat";
import LegendItem from "../LegendItem/LegendItem";
import { LegendProps } from "../LegendGroup";
const LegendLines: FC<LegendProps> = ({ labels, isAnomalyView, duplicateFields, onChange }) => {
return (
<div className="vm-legend-item-container">
{labels.map((legendItem) =>
<LegendItem
key={legendItem.label}
legend={legendItem}
isAnomalyView={isAnomalyView}
duplicateFields={duplicateFields}
onChange={onChange}
/>
)}
</div>
);
};
export default LegendLines;

View File

@@ -1,87 +0,0 @@
import React, { FC, useMemo } from "preact/compat";
import { LegendProps } from "../LegendGroup";
import "./style.scss";
import { LegendItemType } from "../../../../../types";
import { MouseEvent } from "react";
import classNames from "classnames";
import get from "lodash.get";
import { STATS_ORDER } from "../../../../../constants/graph";
import { useShowStats } from "../hooks/useShowStats";
const statsColumns = STATS_ORDER.map(k => ({
key: `statsFormatted.${k}`,
title: k
}));
const LegendTable: FC<LegendProps> = ({ labels, duplicateFields, onChange }) => {
const { hideStats } = useShowStats();
const stats = hideStats ? [] : statsColumns;
const fields = useMemo(() => {
const fields = [...new Set(labels.flatMap(item => Object.keys(item.freeFormFields)))]
.map(f => ({ key: `freeFormFields.${f}`, title: f }));
return duplicateFields?.length
? fields.filter(f => !duplicateFields.includes(f.title))
: fields;
}, [labels, duplicateFields]);
const columns = fields.concat(stats);
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLTableRowElement>) => {
onChange && onChange(legend, e.ctrlKey || e.metaKey);
};
return (
<div className="vm-legend-table__wrapper">
<table className="vm-legend-table">
<thead className="vm-legend-table-thead">
<tr className="vm-legend-table-row vm-legend-table_thead">
<th className="vm-legend-table-col vm-legend-table-col_marker vm-legend-table-col_thead"/>
{columns.map((col) => (
<th
key={col.key}
className="vm-legend-table-col vm-legend-table-col_thead"
>
{col.title}
</th>
))}
</tr>
</thead>
<tbody className="vm-legend-table-tbody">
{labels.map(row => (
<tr
key={row.label}
className={classNames({
"vm-legend-table-row": true,
"vm-legend-table-row_tbody": true,
"vm-legend-table-row_exclude": !row.checked
})}
onClick={createHandlerClick(row)}
>
<td className="vm-legend-table-col vm-legend-table-col_marker">
<div
className="vm-legend-item__marker"
style={{ backgroundColor: row.color }}
/>
</td>
{columns.map((col) => (
<td
key={`${col.key}_${row.label}`}
className="vm-legend-table-col"
>
<span className="vm-legend-table-col__content">
{get(row, col.key)}
</span>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
export default LegendTable;

View File

@@ -1,64 +0,0 @@
@use "src/styles/variables" as *;
.vm-legend-table {
table-layout: auto;
max-width: 100%;
font-size: $font-size-small;
&__wrapper {
width: 100%;
max-height: 50vh;
overflow: auto;
}
&-row {
&_tbody {
cursor: pointer;
transition: 0.3s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
&_exclude {
text-decoration: line-through;
opacity: 0.2;
}
}
&-col {
vertical-align: middle;
text-align: left;
padding: calc($padding-global / 2);
overflow-wrap: anywhere;
&_thead {
position: sticky;
top: 0;
left: calc(14px + $padding-global);
font-weight: bold;
z-index: 2;
white-space: nowrap;
background: $color-background-block;
}
&_marker {
position: sticky;
left: 0;
width: calc(14px + $padding-global);
padding: 0 calc($padding-global / 2);
}
&_marker:not(th) {
z-index: 1;
}
&__content {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

View File

@@ -1,38 +0,0 @@
import { useMemo } from "preact/compat";
import { LegendItemType } from "../../../../../types";
import { QueryGroup } from "../Legend";
type Props = {
labels: LegendItemType[];
query: string[];
groupByLabel?: string;
}
export const useGroupSeries = ({ labels, query, groupByLabel }: Props) => {
return useMemo(() => {
return getGroupSeries(labels, query, groupByLabel);
}, [labels, query, groupByLabel]);
};
const getGroupSeries = (
labels: LegendItemType[],
query: string[],
groupByLabel?: string
): QueryGroup[] => {
const groupMap = new Map<string | number, QueryGroup>();
for (const label of labels) {
const groupKey = groupByLabel
? `${groupByLabel}="${label.freeFormFields[groupByLabel] ?? ""}"`
: `${query[label.group - 1]}`;
if (!groupMap.has(groupKey)) {
groupMap.set(groupKey, { group: String(groupKey), items: [] });
}
const groupEntry = groupMap.get(groupKey) as QueryGroup;
groupEntry.items.push(label);
}
return Array.from(groupMap.values());
};

View File

@@ -1,49 +0,0 @@
import { useEffect, useMemo, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
import { LegendItemType } from "../../../../../types";
const urlKey = LegendQueryParams.hideDuplicates;
export const useHideDuplicateFields = (labels?: LegendItemType[]) => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) === "true";
const [hideDuplicates, setHideDuplicates] = useState(valueFromUrl);
const onChange = (show: boolean) => {
if (!show) {
searchParams.set(urlKey, "true");
} else {
searchParams.delete(urlKey);
}
setHideDuplicates(!show);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey) === "true";
if (value !== hideDuplicates) {
setHideDuplicates(value);
}
}, [searchParams]);
const duplicateFields = useMemo(() => {
if (!hideDuplicates || !labels?.length || labels?.length < 2) {
return [];
}
const allKeys = [...new Set(labels.flatMap(l => Object.keys(l.freeFormFields || {})))];
return allKeys.filter(key => {
const firstValue = labels.find(l => l.freeFormFields[key])?.freeFormFields[key];
return labels.every(l => l.freeFormFields[key] === firstValue);
});
}, [labels, hideDuplicates]);
return {
hideDuplicates,
onChange,
duplicateFields
};
};

View File

@@ -1,38 +0,0 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
const urlKey = LegendQueryParams.format;
export const useLegendFormat = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) || "";
const [format, setFormat] = useState(valueFromUrl);
const onChange = (value: string) => {
setFormat(value);
};
const onApply = () => {
if (format) {
searchParams.set(urlKey, format);
} else {
searchParams.delete(urlKey);
}
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey);
if (value !== format) {
setFormat(value || "");
}
}, [searchParams]);
return {
format,
onChange,
onApply
};
};

View File

@@ -1,34 +0,0 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
import { WITHOUT_GROUPING } from "../../../../../constants/logs";
const urlKey = LegendQueryParams.group;
export const useLegendGroup = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) || "";
const [groupByLabel, setGroupByLabel] = useState(valueFromUrl);
const onChange =(value: string) => {
if (value && value !== WITHOUT_GROUPING) {
searchParams.set(urlKey, value);
} else {
searchParams.delete(urlKey);
}
setGroupByLabel(value);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey);
if (value !== groupByLabel) {
setGroupByLabel(value || "");
}
}, [searchParams]);
return {
groupByLabel,
onChange,
};
};

View File

@@ -1,41 +0,0 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
export enum LegendDisplayType {
table = "table",
lines = "lines"
}
const urlKey = LegendQueryParams.view;
export const useLegendView = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) as LegendDisplayType;
const [view, setView] = useState<LegendDisplayType>(valueFromUrl || LegendDisplayType.lines);
const onChange = (type: LegendDisplayType) => {
if (type === LegendDisplayType.table) {
searchParams.set(urlKey, type);
} else {
searchParams.delete(urlKey);
}
setView(type);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey);
if (value !== view) {
setView(value as LegendDisplayType);
}
}, [searchParams]);
return {
view,
isLinesView: view === LegendDisplayType.lines,
isTableView: view === LegendDisplayType.table,
onChange
};
};

View File

@@ -1,34 +0,0 @@
import { useEffect, useState } from "preact/compat";
import { useSearchParams } from "react-router-dom";
import { LegendQueryParams } from "../types";
const urlKey = LegendQueryParams.hideStats;
export const useShowStats = () => {
const [searchParams, setSearchParams] = useSearchParams();
const valueFromUrl = searchParams.get(urlKey) === "true";
const [hideStats, setHideStats] = useState(valueFromUrl);
const onChange = (showName: boolean) => {
if (!showName) {
searchParams.set(urlKey, "true");
} else {
searchParams.delete(urlKey);
}
setHideStats(!showName);
setSearchParams(searchParams);
};
useEffect(() => {
const value = searchParams.get(urlKey) === "true";
if (value !== hideStats) {
setHideStats(value);
}
}, [searchParams]);
return {
hideStats,
onChange
};
};

View File

@@ -3,9 +3,8 @@
.vm-legend {
position: relative;
display: flex;
flex-direction: column;
flex-wrap: wrap;
cursor: default;
width: 100%;
&-group {
min-width: 23%;
@@ -18,12 +17,13 @@
padding: $padding-small;
margin-bottom: 1px;
border-bottom: $border-divider;
font-size: $font-size-small;
color: $color-text-secondary;
b {
margin-left: $padding-small;
color: $color-text;
&__count {
font-weight: bold;
margin-right: $padding-small;
}
&__query {
}
}
}

View File

@@ -1,7 +0,0 @@
export enum LegendQueryParams {
view = "legend_view",
hideDuplicates = "legend_hide_duplicates",
hideStats = "legend_hide_stats",
format = "legend_format",
group = "legend_group"
}

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