mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-18 09:16:35 +03:00
Compare commits
2 Commits
vmui/fix-i
...
test-tmp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d7d17d192 | ||
|
|
0a8b4281e5 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
2
Makefile
2
Makefile
@@ -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`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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\"}",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"}`)
|
||||
}
|
||||
|
||||
@@ -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/"):
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"}`)
|
||||
)
|
||||
|
||||
12
app/vlselect/vmui/asset-manifest.json
Normal file
12
app/vlselect/vmui/asset-manifest.json
Normal 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
@@ -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
@@ -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>
|
||||
1
app/vlselect/vmui/static/css/main.02a1c6cb.css
Normal file
1
app/vlselect/vmui/static/css/main.02a1c6cb.css
Normal file
File diff suppressed because one or more lines are too long
1
app/vlselect/vmui/static/js/685.f772060c.chunk.js
Normal file
1
app/vlselect/vmui/static/js/685.f772060c.chunk.js
Normal file
File diff suppressed because one or more lines are too long
2
app/vlselect/vmui/static/js/main.55c8060b.js
Normal file
2
app/vlselect/vmui/static/js/main.55c8060b.js
Normal file
File diff suppressed because one or more lines are too long
38
app/vlselect/vmui/static/js/main.55c8060b.js.LICENSE.txt
Normal file
38
app/vlselect/vmui/static/js/main.55c8060b.js.LICENSE.txt
Normal 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
|
||||
*/
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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, "")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ¬ifierMetrics{
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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'")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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. "+
|
||||
|
||||
@@ -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]
|
||||
|
||||
13
app/vmselect/vmui/asset-manifest.json
Normal file
13
app/vmselect/vmui/asset-manifest.json
Normal 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
@@ -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
@@ -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>
|
||||
1
app/vmselect/vmui/static/css/main.7fa18e1b.css
Normal file
1
app/vmselect/vmui/static/css/main.7fa18e1b.css
Normal file
File diff suppressed because one or more lines are too long
1
app/vmselect/vmui/static/js/685.f772060c.chunk.js
Normal file
1
app/vmselect/vmui/static/js/685.f772060c.chunk.js
Normal file
File diff suppressed because one or more lines are too long
2
app/vmselect/vmui/static/js/main.ba08300f.js
Normal file
2
app/vmselect/vmui/static/js/main.ba08300f.js
Normal file
File diff suppressed because one or more lines are too long
38
app/vmselect/vmui/static/js/main.ba08300f.js.LICENSE.txt
Normal file
38
app/vmselect/vmui/static/js/main.ba08300f.js.LICENSE.txt
Normal 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
|
||||
*/
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_APP_TYPE=victoriametrics
|
||||
FAST_REFRESH=false
|
||||
@@ -1 +0,0 @@
|
||||
VITE_APP_TYPE=victorialogs
|
||||
@@ -1 +0,0 @@
|
||||
VITE_APP_TYPE=vmanomaly
|
||||
48
app/vmui/packages/vmui/.eslintrc.js
Normal file
48
app/vmui/packages/vmui/.eslintrc.js
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
42
app/vmui/packages/vmui/config-overrides.js
Normal file
42
app/vmui/packages/vmui/config-overrides.js
Normal 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";
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
},
|
||||
}];
|
||||
20119
app/vmui/packages/vmui/package-lock.json
generated
20119
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,4 @@ const AppAnomaly: FC = () => {
|
||||
</>;
|
||||
};
|
||||
|
||||
export default AppAnomaly;
|
||||
export default AppAnomaly;
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>;
|
||||
};
|
||||
|
||||
@@ -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 {{label_name}} 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 && <> {</>}
|
||||
{!!freeFormFields.length && <>{</>}
|
||||
{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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user