Compare commits

..

2 Commits

Author SHA1 Message Date
Nikolay
3f611b9e31 Update lib/persistentqueue/persistentqueue.go
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Signed-off-by: Nikolay <nik@victoriametrics.com>
2026-07-15 22:09:59 +02:00
f41gh7
0439e50143 lib/persistentqueue: add attempt to restore damaged tail
Previously, if queue chunk file was corrupted at ungraceful shutdown,
writer offset could be bigger than last flushed data block. It could
result into discard of the whole queue.

 This commit adds check for this case, if chunk file size is smaller
than queue writer offset. If so vmagent validates chunk file size and
uses last correct block as writer offset.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11192
2026-07-15 21:54:18 +02:00
790 changed files with 14949 additions and 31185 deletions

View File

@@ -73,19 +73,11 @@ jobs:
id: go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
cache: false
cache-dependency-path: |
go.sum
Makefile
app/**/Makefile
go-version-file: 'go.mod'
- name: Cache Go build artifacts
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-build-${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('go.sum', 'Makefile') }}
restore-keys: |
go-build-${{ matrix.os }}-${{ matrix.arch }}-
- run: go version
- name: Build victoria-metrics for ${{ matrix.os }}-${{ matrix.arch }}

View File

@@ -54,14 +54,14 @@ jobs:
restore-keys: go-artifacts-${{ runner.os }}-codeql-analyze-${{ steps.go.outputs.go-version }}-
- name: Initialize CodeQL
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: 'language:go'

View File

@@ -26,7 +26,9 @@ jobs:
vmui-checks:
name: VMUI Checks (lint, test, typecheck)
permissions:
checks: write
contents: read
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Code checkout
@@ -68,6 +70,12 @@ jobs:
env:
VMUI_SKIP_INSTALL: true
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@d57a1193d4c59cbfbf3f86c271f42612f9dbd9e9 # 3.0.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
report-json: app/vmui/packages/vmui/vmui-lint-report.json
- name: Check overall status
run: |
echo "Lint status: ${{ steps.lint.outcome }}"

View File

@@ -2,6 +2,7 @@
[![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)](https://github.com/VictoriaMetrics/VictoriaMetrics/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/victoriametrics/victoria-metrics?label=&logo=docker&logoColor=white&labelColor=2496ED&color=2496ED&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fvictoriametrics%2Fvictoria-metrics)](https://hub.docker.com/u/victoriametrics)
[![Go Report](https://goreportcard.com/badge/github.com/VictoriaMetrics/VictoriaMetrics?link=https%3A%2F%2Fgoreportcard.com%2Freport%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics)](https://goreportcard.com/report/github.com/VictoriaMetrics/VictoriaMetrics)
[![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/build.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/build.yml)
[![License](https://img.shields.io/github/license/VictoriaMetrics/VictoriaMetrics?labelColor=green&label=&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Fblob%2Fmaster%2FLICENSE)](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE)
[![Join Slack](https://img.shields.io/badge/Join%20Slack-4A154B?logo=slack)](https://slack.victoriametrics.com)

View File

@@ -63,7 +63,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
initSecretFlags()
buildinfo.Init()
logger.Init()
@@ -171,9 +170,3 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/
`
flagutil.Usage(s)
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
pushmetrics.InitSecretFlags()
vmselect.InitSecretFlags()
}

View File

@@ -84,7 +84,7 @@ var (
maxLabelNameLen = flag.Int("maxLabelNameLen", 0, "The maximum length of label names in the accepted time series. Series with longer label name are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_name\"} metric at /metrics page is incremented")
maxLabelValueLen = flag.Int("maxLabelValueLen", 0, "The maximum length of label values in the accepted time series. Series with longer label value are ignored. In this case the vm_rows_ignored_total{reason=\"too_long_label_value\"} metric at /metrics page is incremented")
enableMultitenancyViaHeaders = flag.Bool("enableMultitenancyViaHeaders", true, "Enables multitenancy via HTTP headers. "+
enableMultitenancyViaHeaders = flag.Bool("enableMultitenancyViaHeaders", false, "Enables multitenancy via HTTP headers. "+
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#multitenancy")
)
@@ -115,7 +115,7 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
initSecretFlags()
remotewrite.InitSecretFlags()
buildinfo.Init()
logger.Init()
opentelemetry.Init()
@@ -462,9 +462,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
case "/prometheus/metric-relabel-debug", "/metric-relabel-debug":
promscrapeMetricRelabelDebugRequests.Inc()
rwGlobalRelabelConfigs := remotewrite.GetRemoteWriteRelabelConfigString()
rwURLRelabelConfigss := remotewrite.GetURLRelabelConfigString()
promscrape.WriteMetricRelabelDebug(w, r, rwGlobalRelabelConfigs, rwURLRelabelConfigss)
promscrape.WriteMetricRelabelDebug(w, r)
return true
case "/prometheus/target-relabel-debug", "/target-relabel-debug":
promscrapeTargetRelabelDebugRequests.Inc()
@@ -843,9 +841,3 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
`
flagutil.Usage(s)
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
remotewrite.InitSecretFlags()
pushmetrics.InitSecretFlags()
}

View File

@@ -52,7 +52,6 @@ func setUp() {
func tearDown() {
protoparserutil.StopUnmarshalWorkers()
remotewrite.Stop()
srv.Close()
logger.ResetOutputForTest()
tmpDataDir := flag.Lookup("remoteWrite.tmpDataPath").Value.String()

View File

@@ -151,23 +151,17 @@ func newHTTPClient(argIdx int, remoteWriteURL, sanitizedURL string, fq *persiste
}
tr.Proxy = http.ProxyURL(pu)
}
hc := &http.Client{
Transport: authCfg.NewRoundTripper(tr),
Timeout: sendTimeout.GetOptionalArg(argIdx),
}
rwURL, err := url.Parse(remoteWriteURL)
if err != nil {
logger.Fatalf("BUG: cannot parse already parsed -remoteWrite.url=%q: %s", remoteWriteURL, err)
}
hc.Transport, rwURL = httputil.NewLoadBalancerTransport(hc.Transport, rwURL)
retryMaxIntervalFlag := retryMaxTime
if retryMaxInterval.String() != "" {
retryMaxIntervalFlag = retryMaxInterval
}
c := &client{
sanitizedURL: sanitizedURL,
remoteWriteURL: rwURL.String(),
remoteWriteURL: remoteWriteURL,
authCfg: authCfg,
awsCfg: awsCfg,
fq: fq,

View File

@@ -1,108 +0,0 @@
package remotewrite
import (
"crypto/sha256"
"encoding/hex"
"slices"
"strings"
"sync"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promrelabel"
)
type obfuscateLabelsCtx struct {
labels []prompb.Label
// buf holds allocations for cached results below
buf []byte
// cacheResults maps original label values to their SHA-256 hex digests,
// avoiding redundant hashing of repeated values within a single batch.
cacheResults map[string]string
}
func (olctx *obfuscateLabelsCtx) reset() {
promrelabel.CleanLabels(olctx.labels)
olctx.labels = olctx.labels[:0]
clear(olctx.cacheResults)
olctx.buf = olctx.buf[:0]
}
var obfuscateLabelsCtxPool = &sync.Pool{
New: func() any {
return &obfuscateLabelsCtx{
cacheResults: make(map[string]string),
}
},
}
func getObfuscateLabelsCtx() *obfuscateLabelsCtx {
return obfuscateLabelsCtxPool.Get().(*obfuscateLabelsCtx)
}
func putObfuscateLabelsCtx(ctx *obfuscateLabelsCtx) {
ctx.reset()
obfuscateLabelsCtxPool.Put(ctx)
}
func (olctx *obfuscateLabelsCtx) obfuscate(tss []prompb.TimeSeries, obfuscateLabels []string) []prompb.TimeSeries {
if len(obfuscateLabels) == 0 || len(tss) == 0 {
return tss
}
labels := olctx.labels
for i := range tss {
ts := &tss[i]
labelsLen := len(labels)
labels = append(labels, ts.Labels...)
found := false
for _, labelName := range obfuscateLabels {
tmp := promrelabel.GetLabelByName(labels[labelsLen:], labelName)
if tmp == nil {
continue
}
found = true
if obfuscatedValue, ok := olctx.cacheResults[tmp.Value]; ok {
// fast path: the obfuscated result was calculated before
tmp.Value = obfuscatedValue
continue
}
digest := sha256.Sum256(bytesutil.ToUnsafeBytes(tmp.Value))
buf := olctx.buf
bufLen := len(buf)
buf = hex.AppendEncode(buf, digest[:])
obfuscatedValue := bytesutil.ToUnsafeString(buf[bufLen:])
olctx.buf = buf
olctx.cacheResults[tmp.Value] = obfuscatedValue
tmp.Value = obfuscatedValue
}
if found {
ts.Labels = labels[labelsLen:]
} else {
labels = labels[:labelsLen]
}
}
olctx.labels = labels
return tss
}
func (rwctx *remoteWriteCtx) initObfuscateLabels() {
if len(*obfuscateLabels) == 0 {
return
}
idx := rwctx.idx
rwObfuscateLabels := obfuscateLabels.GetOptionalArg(idx)
rwObfuscateLabelsList := strings.Split(rwObfuscateLabels, "^^")
for _, label := range rwObfuscateLabelsList {
if label == "" {
continue
}
if !slices.Contains(rwctx.obfuscateLabels, label) {
rwctx.obfuscateLabels = append(rwctx.obfuscateLabels, label)
}
}
}

View File

@@ -1,186 +0,0 @@
package remotewrite
import (
"crypto/sha256"
"encoding/hex"
"reflect"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/metrics"
)
func TestRemoteWriteObfuscateLabels(t *testing.T) {
f := func(obfuscateLabelList string, inputTss []prompb.TimeSeries, expectedTss []prompb.TimeSeries) {
t.Helper()
rwctx := &remoteWriteCtx{
idx: 0,
}
olctx := &obfuscateLabelsCtx{
cacheResults: make(map[string]string),
}
defer metrics.UnregisterAllMetrics()
originValue := *obfuscateLabels
defer func() {
*obfuscateLabels = originValue
}()
*obfuscateLabels = []string{obfuscateLabelList}
rwctx.initObfuscateLabels()
outputTss := olctx.obfuscate(inputTss, rwctx.obfuscateLabels)
if !reflect.DeepEqual(expectedTss, outputTss) {
t.Fatalf("unexpected samples;\ngot\n%v\nwant\n%v", outputTss, expectedTss)
}
}
sha256Result := func(str string) string {
sha256Result := sha256.Sum256([]byte(str))
return hex.EncodeToString(sha256Result[:])
}
// 1. obfuscation is not set.
f("",
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
)
// 1. obfuscation is set for another rwctx.
f(",ip",
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
)
// 2. obfuscate the value of "ip" label
f("ip",
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: sha256Result("123")},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
)
// 3. obfuscate the values of "ip" and "instance"
f("ip^^instance",
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "1234"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
{
Labels: []prompb.Label{
{Name: "job", Value: "123"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: sha256Result("123")},
{Name: "instance", Value: sha256Result("1234")},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
{
Labels: []prompb.Label{
{Name: "job", Value: "123"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
)
// 4. duplicate label names in config must produce single SHA-256, not double
f("ip^^ip",
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
[]prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: sha256Result("123")},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
},
)
}

View File

@@ -1,94 +0,0 @@
package remotewrite
import (
"crypto/sha256"
"encoding/hex"
"reflect"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/metrics"
)
func BenchmarkRemoteWriteObfuscateLabels(b *testing.B) {
originValue := *obfuscateLabels
defer func() {
*obfuscateLabels = originValue
}()
*obfuscateLabels = []string{"ip^^instance"}
sha256Result := func(str string) string {
sha256Result := sha256.Sum256([]byte(str))
return hex.EncodeToString(sha256Result[:])
}
expected := []prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: sha256Result("123")},
{Name: "instance", Value: sha256Result("12345")},
{Name: "__name__", Value: "http_requests_total"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
{
Labels: []prompb.Label{
{Name: "ip", Value: sha256Result("1236")},
{Name: "instance", Value: sha256Result("some-long-instante-string")},
{Name: "__name__", Value: "concurrent_requests"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
}
defer metrics.UnregisterAllMetrics()
inputTss := []prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "ip", Value: "123"},
{Name: "instance", Value: "12345"},
{Name: "__name__", Value: "http_requests_total"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
{
Labels: []prompb.Label{
{Name: "ip", Value: "1236"},
{Name: "instance", Value: "some-long-instante-string"},
{Name: "__name__", Value: "concurrent_requests"},
},
Samples: []prompb.Sample{
{Value: 1, Timestamp: 0},
},
},
}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
rwctx := &remoteWriteCtx{
idx: 0,
}
olctx := &obfuscateLabelsCtx{
cacheResults: make(map[string]string),
}
rwctx.initObfuscateLabels()
var localTss []prompb.TimeSeries
for pb.Next() {
// always make a shallow copy because obfuscate changes input
localTss = localTss[:0]
localTss = append(localTss, inputTss...)
olctx.reset()
outputTss := olctx.obfuscate(localTss, rwctx.obfuscateLabels)
if !reflect.DeepEqual(expected, outputTss) {
b.Fatalf("unexpected output: got: \n%v\n want: \n%v\n", outputTss, expected)
}
}
})
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/VictoriaMetrics/metrics"
"gopkg.in/yaml.v2"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -83,16 +82,6 @@ func WriteRelabelConfigData(w io.Writer) {
_, _ = w.Write(*p)
}
// GetRemoteWriteRelabelConfigString returns -remoteWrite.relabelConfig contents in string
func GetRemoteWriteRelabelConfigString() string {
var bb bytesutil.ByteBuffer
WriteRelabelConfigData(&bb)
if bb.Len() == 0 {
return ""
}
return string(bb.B)
}
// WriteURLRelabelConfigData writes -remoteWrite.urlRelabelConfig contents to w
func WriteURLRelabelConfigData(w io.Writer) {
p := remoteWriteURLRelabelConfigData.Load()
@@ -119,24 +108,6 @@ func WriteURLRelabelConfigData(w io.Writer) {
_, _ = w.Write(d)
}
// GetURLRelabelConfigString returns -remoteWrite.urlRelabelConfig contents in []string
func GetURLRelabelConfigString() []string {
p := remoteWriteURLRelabelConfigData.Load()
if p == nil {
return nil
}
var ss []string
for i := range *remoteWriteURLs {
cfgData := (*p)[i]
var cfgDataBytes []byte
if cfgData != nil {
cfgDataBytes, _ = yaml.Marshal(cfgData)
}
ss = append(ss, string(cfgDataBytes))
}
return ss
}
func reloadRelabelConfigs() {
rcs := allRelabelConfigs.Load()
if !rcs.isSet() {

View File

@@ -107,12 +107,7 @@ var (
"By default, metadata sending is controlled by the global -enableMetadata flag")
enableMdx = flagutil.NewArrayBool("remoteWrite.mdx.enable", "Whether to only retain metrics from VictoriaMetrics services before sending them to the corresponding -remoteWrite.url. "+
"Can be combined with -remoteWrite.obfuscateLabels to hide sensitive label values in the forwarded metrics. "+
"Please see https://docs.victoriametrics.com/victoriametrics/vmagent/#monitoring-data-exchange")
obfuscateLabels = flagutil.NewArrayString("remoteWrite.obfuscateLabels", "List of label names whose values will be obfuscated before being sent to the corresponding -remoteWrite.url. "+
"Multiple label names should be separated by `^^`, e.g. \"job^^instance,ip\". "+
"Can be combined with -remoteWrite.mdx.enable to hide sensitive label values in VictoriaMetrics self-monitoring metrics. "+
"Please see https://docs.victoriametrics.com/victoriametrics/vmagent/#obfuscating-label-values")
)
var (
@@ -156,8 +151,7 @@ var maxQueues = cgroup.AvailableCPUs() * 16
const persistentQueueDirname = "persistent-queue"
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
// It should run before logger initialization and package Init() (if exists).
// InitSecretFlags must be called after flag.Parse and before any logging.
func InitSecretFlags() {
if !*showRemoteWriteURL {
// remoteWrite.url can contain authentication codes, so hide it at `/metrics` output.
@@ -246,8 +240,6 @@ func Init() {
dropDanglingQueues()
// Start config reloader.
configReloaderStopCh = make(chan struct{})
configReloaderWG = sync.WaitGroup{}
configReloaderWG.Go(func() {
for {
select {
@@ -334,7 +326,7 @@ func initRemoteWriteCtxs(urls []string) {
}
var (
configReloaderStopCh chan struct{}
configReloaderStopCh = make(chan struct{})
configReloaderWG sync.WaitGroup
)
@@ -889,8 +881,6 @@ type remoteWriteCtx struct {
pss []*pendingSeries
pssNextIdx atomic.Uint64
obfuscateLabels []string
rowsPushedAfterRelabel *metrics.Counter
rowsDroppedByRelabel *metrics.Counter
mdxRowsPreserved *metrics.Counter
@@ -1005,7 +995,6 @@ func newRemoteWriteCtx(argIdx int, remoteWriteURL *url.URL, sanitizedURL string)
rowsDroppedOnPushFailure: metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_samples_dropped_total{path=%q,url=%q}`, queuePath, sanitizedURL)),
}
rwctx.initStreamAggrConfig()
rwctx.initObfuscateLabels()
if enableMdx.GetOptionalArg(argIdx) {
mdxFilter := mdx.NewFilter()
@@ -1209,41 +1198,24 @@ func (rwctx *remoteWriteCtx) tryPushMetadataInternal(mms []prompb.MetricMetadata
func (rwctx *remoteWriteCtx) tryPushTimeSeriesInternal(tss []prompb.TimeSeries) bool {
var rctx *relabelCtx
var v *[]prompb.TimeSeries
var olctx *obfuscateLabelsCtx
defer func() {
if v != nil {
*v = prompb.ResetTimeSeries(tss)
tssPool.Put(v)
}
if rctx != nil {
putRelabelCtx(rctx)
}
if olctx != nil {
putObfuscateLabelsCtx(olctx)
if rctx == nil {
return
}
*v = prompb.ResetTimeSeries(tss)
tssPool.Put(v)
putRelabelCtx(rctx)
}()
copyTimeSeriesIfNeeded := func() {
if v == nil {
v = tssPool.Get().(*[]prompb.TimeSeries)
tss = append(*v, tss...)
}
}
if len(labelsGlobal) > 0 {
// Make a copy of tss before adding extra labels to prevent
// from affecting time series for other remoteWrite.url configs.
rctx = getRelabelCtx()
copyTimeSeriesIfNeeded()
v = tssPool.Get().(*[]prompb.TimeSeries)
tss = append(*v, tss...)
rctx.appendExtraLabels(tss, labelsGlobal)
}
if len(rwctx.obfuscateLabels) != 0 {
copyTimeSeriesIfNeeded()
olctx = getObfuscateLabelsCtx()
tss = olctx.obfuscate(tss, rwctx.obfuscateLabels)
}
pss := rwctx.pss
idx := rwctx.pssNextIdx.Add(1) % uint64(len(pss))

View File

@@ -11,7 +11,6 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
@@ -268,11 +267,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
err = &httpserver.ErrorWithStatusCode{
StatusCode: resp.StatusCode,
Err: fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, ru, body),
}
return nil, err
return nil, fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, ru, body)
}
return resp, nil
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
)
@@ -60,8 +59,7 @@ var (
`Only valid for VictoriaMetrics as the datasource.`)
)
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
// It should run before logger initialization and package Init() (if exists).
// InitSecretFlags must be called after flag.Parse and before any logging
func InitSecretFlags() {
if !*showDatasourceURL {
flagutil.RegisterSecretFlag("datasource.url")
@@ -96,12 +94,6 @@ func Init(extraParams url.Values) (QuerierBuilder, error) {
tr.MaxIdleConns = tr.MaxIdleConnsPerHost
}
tr.IdleConnTimeout = *idleConnectionTimeout
hc := &http.Client{Transport: tr}
datasourceURL, err := url.Parse(*addr)
if err != nil {
logger.Fatalf("BUG: cannot parse already parsed -datasource.url=%q: %s", *addr, err)
}
hc.Transport, datasourceURL = httputil.NewLoadBalancerTransport(tr, datasourceURL)
if extraParams == nil {
extraParams = url.Values{}
@@ -128,9 +120,9 @@ func Init(extraParams url.Values) (QuerierBuilder, error) {
}
return &Client{
c: hc,
c: &http.Client{Transport: tr},
authCfg: authCfg,
datasourceURL: strings.TrimSuffix(datasourceURL.String(), "/"),
datasourceURL: strings.TrimSuffix(*addr, "/"),
appendTypePrefix: *appendTypePrefix,
queryStep: *queryStep,
extraParams: extraParams,

View File

@@ -88,7 +88,10 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
initSecretFlags()
remoteread.InitSecretFlags()
remotewrite.InitSecretFlags()
datasource.InitSecretFlags()
notifier.InitSecretFlags()
buildinfo.Init()
logger.Init()
@@ -435,12 +438,3 @@ func getLastConfigError() error {
defer lastConfigErrMu.RUnlock()
return lastConfigErr
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
remoteread.InitSecretFlags()
remotewrite.InitSecretFlags()
datasource.InitSecretFlags()
notifier.InitSecretFlags()
pushmetrics.InitSecretFlags()
}

View File

@@ -31,8 +31,6 @@ type Alert struct {
State AlertState
// Expr contains expression that was executed to generate the Alert
Expr string
// Interval contains the evaluation interval of the Alert's group
Interval time.Duration
// ActiveAt defines the moment of time when Alert has become active
ActiveAt time.Time
// Start defines the moment of time when Alert has become firing
@@ -86,7 +84,6 @@ type AlertTplData struct {
Labels map[string]string
Value float64
Expr string
Interval time.Duration
AlertID uint64
GroupID uint64
ActiveAt time.Time
@@ -99,7 +96,6 @@ var tplHeaders = []string{
"{{ $type := .Type }}",
"{{ $labels := .Labels }}",
"{{ $expr := .Expr }}",
"{{ $interval := .Interval }}",
"{{ $externalLabels := .ExternalLabels }}",
"{{ $externalURL := .ExternalURL }}",
"{{ $alertID := .AlertID }}",
@@ -119,7 +115,6 @@ func (a *Alert) ExecTemplate(q templates.QueryFn, labels, annotations map[string
Type: a.Type,
Labels: labels,
Expr: a.Expr,
Interval: a.Interval,
AlertID: a.ID,
GroupID: a.GroupID,
ActiveAt: a.ActiveAt,

View File

@@ -129,17 +129,6 @@ func TestAlertExecTemplate(t *testing.T) {
"exprEscapedHTML": "vm_rows{&quot;label&quot;=&quot;bar&quot;}&lt;0",
})
// interval-template
f(&Alert{
Interval: 10 * time.Second,
}, map[string]string{
"interval": "{{ .Interval }}",
"intervalVariable": "{{ $interval }}",
}, map[string]string{
"interval": "10s",
"intervalVariable": "10s",
})
// query
f(&Alert{
Expr: `vm_rows{"label"="bar"}>0`,

View File

@@ -189,8 +189,7 @@ func Init(extLabels map[string]string, extURL string) error {
return nil
}
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
// It should run before logger initialization and package Init() (if exists).
// InitSecretFlags must be called after flag.Parse and before any logging
func InitSecretFlags() {
if !*showNotifierURL {
flagutil.RegisterSecretFlag("notifier.url")

View File

@@ -4,14 +4,12 @@ import (
"flag"
"fmt"
"net/http"
"net/url"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/vmalertutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
)
@@ -56,8 +54,7 @@ var (
oauth2Scopes = flag.String("remoteRead.oauth2.scopes", "", "Optional OAuth2 scopes to use for -remoteRead.url. Scopes must be delimited by ';'.")
)
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
// It should run before logger initialization and package Init() (if exists).
// InitSecretFlags must be called after flag.Parse and before any logging
func InitSecretFlags() {
if !*showRemoteReadURL {
flagutil.RegisterSecretFlag("remoteRead.url")
@@ -79,13 +76,7 @@ func Init() (datasource.QuerierBuilder, error) {
return nil, fmt.Errorf("failed to create transport for -remoteRead.url=%q: %w", *addr, err)
}
tr.IdleConnTimeout = *idleConnectionTimeout
c := &http.Client{Transport: tr}
rrURL, err := url.Parse(*addr)
if err != nil {
logger.Fatalf("BUG: cannot parse already parsed -remoteRead.url=%q: %s", *addr, err)
}
c.Transport, rrURL = httputil.NewLoadBalancerTransport(tr, rrURL)
endpointParams, err := flagutil.ParseJSONMap(*oauth2EndpointParams)
if err != nil {
return nil, fmt.Errorf("cannot parse JSON for -remoteRead.oauth2.endpointParams=%s: %w", *oauth2EndpointParams, err)
@@ -98,5 +89,6 @@ func Init() (datasource.QuerierBuilder, error) {
if err != nil {
return nil, fmt.Errorf("failed to configure auth: %w", err)
}
return datasource.NewPrometheusClient(rrURL.String(), authCfg, false, c), nil
c := &http.Client{Transport: tr}
return datasource.NewPrometheusClient(*addr, authCfg, false, c), nil
}

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"sync"
@@ -112,18 +111,12 @@ func NewClient(ctx context.Context, cfg Config) (*Client, error) {
if cfg.Concurrency > 0 {
cc = cfg.Concurrency
}
hc := &http.Client{
Timeout: *sendTimeout,
Transport: cfg.Transport,
}
rwURL, err := url.Parse(cfg.Addr)
if err != nil {
logger.Fatalf("cannot parse already parsed -remoteWrite.url=%q: %s", cfg.Addr, err)
}
hc.Transport, rwURL = httputil.NewLoadBalancerTransport(hc.Transport, rwURL)
c := &Client{
c: hc,
addr: strings.TrimSuffix(rwURL.String(), "/"),
c: &http.Client{
Timeout: *sendTimeout,
Transport: cfg.Transport,
},
addr: strings.TrimSuffix(cfg.Addr, "/"),
authCfg: cfg.AuthCfg,
flushInterval: cfg.FlushInterval,
maxBatchSize: cfg.MaxBatchSize,

View File

@@ -57,8 +57,7 @@ var (
oauth2Scopes = flag.String("remoteWrite.oauth2.scopes", "", "Optional OAuth2 scopes to use for -notifier.url. Scopes must be delimited by ';'.")
)
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
// It should run before logger initialization and package Init() (if exists).
// InitSecretFlags must be called after flag.Parse and before any logging
func InitSecretFlags() {
if !*showRemoteWriteURL {
flagutil.RegisterSecretFlag("remoteWrite.url")

View File

@@ -25,13 +25,11 @@ var (
replayMaxDatapoints = flag.Int("replay.maxDatapointsPerQuery", 1e3,
"Max number of data points expected in one request. It affects the max time range for every '/query_range' request during the replay. The higher the value, the less requests will be made during replay.")
replayRuleRetryAttempts = flag.Int("replay.ruleRetryAttempts", 5,
"Defines how many retries to make before giving up on rule if request for it returns a retriable error.")
"Defines how many retries to make before giving up on rule if request for it returns an error.")
disableProgressBar = flag.Bool("replay.disableProgressBar", false, "Whether to disable rendering progress bars during the replay. "+
"Progress bar rendering might be verbose or break the logs parsing, so it is recommended to be disabled when not used in interactive mode.")
ruleEvaluationConcurrency = flag.Int("replay.ruleEvaluationConcurrency", 1, "The maximum number of concurrent '/query_range' requests when replay recording rule or alerting rule with for=0. "+
"Increasing this value when replaying for a long time, since each request is limited by -replay.maxDatapointsPerQuery.")
continueWithExecutionErr = flag.Bool("replay.continueWithExecutionErr", false, "Whether to continue replaying other rules if a rule execution fails with a 400 or 422 response code, "+
"which can happen due to an expression syntax error or a resource limit being hit.")
)
func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewrite.RWClient) (totalRows, droppedRows int, err error) {
@@ -75,7 +73,7 @@ func replay(groupsCfg []config.Group, qb datasource.QuerierBuilder, rw remotewri
for _, cfg := range groupsCfg {
ng := rule.NewGroup(cfg, qb, *evaluationInterval, labels)
totalRows += ng.Replay(tFrom, tTo, rw, *replayMaxDatapoints, *replayRuleRetryAttempts, *replayRulesDelay, *disableProgressBar, *ruleEvaluationConcurrency, *continueWithExecutionErr)
totalRows += ng.Replay(tFrom, tTo, rw, *replayMaxDatapoints, *replayRuleRetryAttempts, *replayRulesDelay, *disableProgressBar, *ruleEvaluationConcurrency)
}
logger.Infof("replay evaluation finished, generated %d samples", totalRows)
if err := rw.Close(); err != nil {

View File

@@ -8,10 +8,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
"github.com/VictoriaMetrics/metricsql"
)
type fakeReplayQuerier struct {
@@ -34,14 +32,6 @@ func (fc *fakeRWClient) Close() error {
}
func (fr *fakeReplayQuerier) QueryRange(_ context.Context, q string, from, to time.Time) (res datasource.Result, err error) {
_, err = metricsql.Parse(q)
if err != nil {
return res, &httpserver.ErrorWithStatusCode{
StatusCode: 422,
Err: err,
}
}
key := fmt.Sprintf("%s+%s", from.Format("15:04:05"), to.Format("15:04:05"))
dps, ok := fr.registry[q]
if !ok {
@@ -285,28 +275,4 @@ func TestReplay(t *testing.T) {
},
},
}, 10)
// rule with wrong expression won't break the other rule with continueWithExecutionErr
continueWithExecutionErrOld := *continueWithExecutionErr
defer func() {
*continueWithExecutionErr = continueWithExecutionErrOld
}()
*continueWithExecutionErr = true
f("2021-01-01T12:00:00.000Z", "2021-01-01T12:02:30.000Z", 1, 1, time.Millisecond, []config.Group{
{Rules: []config.Rule{{Record: "foo", Expr: "sum(up)"}}},
{Rules: []config.Rule{{Record: "bar", Expr: "up ++"}}},
}, &fakeReplayQuerier{
registry: map[string]map[string][]datasource.Metric{
"sum(up)": {
"12:00:00+12:01:00": {
{
Timestamps: []int64{1, 2},
Values: []float64{1, 2},
},
},
"12:01:00+12:02:00": {},
"12:02:00+12:02:30": {},
},
},
}, 2)
}

View File

@@ -530,7 +530,6 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
ar.logDebugf(ts, a, "INACTIVE => PENDING")
}
a.Value = m.Values[0]
a.Interval = ar.EvalInterval
a.Annotations = annotations
a.KeepFiringSince = time.Time{}
continue
@@ -613,7 +612,6 @@ func (ar *AlertingRule) expandAnnotationTemplates(m datasource.Metric, qFn templ
Type: ar.Type.String(),
Labels: ls.origin,
Expr: ar.Expr,
Interval: ar.EvalInterval,
AlertID: hash(ls.processed),
GroupID: ar.GroupID,
ActiveAt: activeAt,
@@ -675,7 +673,6 @@ func (ar *AlertingRule) newAlert(m datasource.Metric, start time.Time, labels, a
Name: ar.Name,
Type: ar.Type.String(),
Expr: ar.Expr,
Interval: ar.EvalInterval,
For: ar.For,
ActiveAt: start,
Value: m.Values[0],

View File

@@ -662,7 +662,6 @@ func TestAlertingRuleExecRange(t *testing.T) {
GroupID: fakeGroup.GetID(),
Name: "for-pending",
Type: config.NewPrometheusType().String(),
Interval: time.Second,
Labels: map[string]string{"alertname": "for-pending"},
Annotations: map[string]string{},
State: notifier.StatePending,
@@ -683,7 +682,6 @@ func TestAlertingRuleExecRange(t *testing.T) {
GroupID: fakeGroup.GetID(),
Name: "for-firing",
Type: config.NewPrometheusType().String(),
Interval: 3 * time.Second,
Labels: map[string]string{"alertname": "for-firing"},
Annotations: map[string]string{},
State: notifier.StateFiring,
@@ -705,7 +703,6 @@ func TestAlertingRuleExecRange(t *testing.T) {
GroupID: fakeGroup.GetID(),
Name: "for-hold-pending",
Type: config.NewPrometheusType().String(),
Interval: time.Second,
Labels: map[string]string{"alertname": "for-hold-pending"},
Annotations: map[string]string{},
State: notifier.StatePending,
@@ -762,7 +759,6 @@ func TestAlertingRuleExecRange(t *testing.T) {
GroupID: fakeGroup.GetID(),
Name: "multi-series",
Type: config.NewPrometheusType().String(),
Interval: 3 * time.Second,
Labels: map[string]string{"alertname": "multi-series"},
Annotations: map[string]string{},
State: notifier.StateFiring,
@@ -775,7 +771,6 @@ func TestAlertingRuleExecRange(t *testing.T) {
GroupID: fakeGroup.GetID(),
Name: "multi-series",
Type: config.NewPrometheusType().String(),
Interval: 3 * time.Second,
Labels: map[string]string{"alertname": "multi-series", "foo": "bar"},
Annotations: map[string]string{},
State: notifier.StatePending,
@@ -1139,8 +1134,7 @@ func TestAlertingRule_Template(t *testing.T) {
fq.Add(metrics...)
fq.SetPartialResponse(isResponsePartial)
ts := time.Unix(3600, 0)
if _, err := rule.exec(context.TODO(), ts, 0); err != nil {
if _, err := rule.exec(context.TODO(), time.Now(), 0); err != nil {
t.Fatalf("unexpected error: %s", err)
}
for hash, expAlert := range alertsExpected {
@@ -1158,14 +1152,12 @@ func TestAlertingRule_Template(t *testing.T) {
}
f(&AlertingRule{
Name: "common",
EvalInterval: time.Hour,
Name: "common",
Labels: map[string]string{
"region": "east",
},
Annotations: map[string]string{
"summary": `{{ $labels.alertname }}: Too high connection number for "{{ $labels.instance }}"`,
"dashboard": `&from={{ ($activeAt.Add (parseDurationTime (printf "-%s" .Interval))).UnixMilli }}&to={{ $activeAt.UnixMilli }}`,
"summary": `{{ $labels.alertname }}: Too high connection number for "{{ $labels.instance }}"`,
},
alerts: make(map[uint64]*notifier.Alert),
}, []datasource.Metric{
@@ -1174,8 +1166,7 @@ func TestAlertingRule_Template(t *testing.T) {
}, false, map[uint64]*notifier.Alert{
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "foo"}): {
Annotations: map[string]string{
"summary": `common: Too high connection number for "foo"`,
"dashboard": "&from=0&to=3600000",
"summary": `common: Too high connection number for "foo"`,
},
Labels: map[string]string{
alertNameLabel: "common",
@@ -1185,8 +1176,7 @@ func TestAlertingRule_Template(t *testing.T) {
},
hash(map[string]string{alertNameLabel: "common", "region": "east", "instance": "bar"}): {
Annotations: map[string]string{
"summary": `common: Too high connection number for "bar"`,
"dashboard": "&from=0&to=3600000",
"summary": `common: Too high connection number for "bar"`,
},
Labels: map[string]string{
alertNameLabel: "common",
@@ -1398,7 +1388,7 @@ func TestAlertingRule_ToLabels(t *testing.T) {
"alertname": "ConfigurationReloadFailure",
"alertgroup": "vmalert",
"pod": "vmalert-0",
"invalid_label": `error evaluating template: template: :1:326: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
"invalid_label": `error evaluating template: template: :1:298: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
}
expectedProcessedLabels := map[string]string{
@@ -1408,7 +1398,7 @@ func TestAlertingRule_ToLabels(t *testing.T) {
"exported_alertname": "ConfigurationReloadFailure",
"group": "vmalert",
"alertgroup": "vmalert",
"invalid_label": `error evaluating template: template: :1:326: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
"invalid_label": `error evaluating template: template: :1:298: executing "" at <.Values.mustRuntimeFail>: can't evaluate field Values in type notifier.tplData`,
}
ls, err := ar.toLabels(metric, nil)

View File

@@ -290,8 +290,6 @@ func (g *Group) updateWith(newGroup *Group) error {
g.Headers = newGroup.Headers
g.NotifierHeaders = newGroup.NotifierHeaders
g.Labels = newGroup.Labels
g.EvalDelay = newGroup.EvalDelay
g.evalAlignment = newGroup.evalAlignment
g.Limit = newGroup.Limit
g.checksum = newGroup.checksum
g.Rules = newRules
@@ -339,7 +337,7 @@ func (g *Group) Init() {
i := g.Interval.Seconds()
return i
})
g.metrics.iterationLimit = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_group_rule_results_limit{%s}`, labels), func() float64 {
g.metrics.iterationLimit = g.metrics.set.NewGauge(fmt.Sprintf(`vmalert_rule_group_results_limit{%s}`, labels), func() float64 {
g.mu.RLock()
limit := g.Limit
g.mu.RUnlock()
@@ -550,7 +548,7 @@ func (g *Group) infof(format string, args ...any) {
}
// Replay performs group replay
func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoint, replayRuleRetryAttempts int, replayDelay time.Duration, disableProgressBar bool, ruleEvaluationConcurrency int, continueWithExecutionErr bool) int {
func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoint, replayRuleRetryAttempts int, replayDelay time.Duration, disableProgressBar bool, ruleEvaluationConcurrency int) int {
var total int
step := g.Interval * time.Duration(maxDataPoint)
ri := rangeIterator{start: start, end: end, step: step}
@@ -578,7 +576,7 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
if !disableProgressBar {
bar = pb.StartNew(iterations)
}
total += replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency, continueWithExecutionErr)
total += replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency)
if bar != nil {
bar.Finish()
}
@@ -600,7 +598,7 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
rule := g.Rules[i]
sem <- struct{}{}
wg.Go(func() {
res <- replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency, continueWithExecutionErr)
res <- replayRuleRange(rule, ri, bar, rw, replayRuleRetryAttempts, ruleEvaluationConcurrency)
<-sem
})
}
@@ -620,7 +618,7 @@ func (g *Group) Replay(start, end time.Time, rw remotewrite.RWClient, maxDataPoi
return total
}
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts, ruleEvaluationConcurrency int, continueWithExecutionErr bool) int {
func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewrite.RWClient, replayRuleRetryAttempts, ruleEvaluationConcurrency int) int {
fmt.Printf("> Rule %q (ID: %d)\n", r, r.ID())
// alerting rule with for>0 can't be replayed concurrently, since the status change might depend on the previous evaluation
// see https://github.com/VictoriaMetrics/VictoriaMetrics/commit/abcb21aa5ee918ba9a4e9cde495dba06e1e9564c
@@ -635,7 +633,7 @@ func replayRuleRange(r Rule, ri rangeIterator, bar *pb.ProgressBar, rw remotewri
start := ri.s
end := ri.e
wg.Go(func() {
n, err := replayRule(r, start, end, rw, replayRuleRetryAttempts, continueWithExecutionErr)
n, err := replayRule(r, start, end, rw, replayRuleRetryAttempts)
if err != nil {
logger.Fatalf("rule %q: %s", r, err)
}

View File

@@ -78,12 +78,6 @@ func TestUpdateWith(t *testing.T) {
if g.Debug != expect.Debug {
t.Fatalf("expected to have debug %v; got %v", expect.Debug, g.Debug)
}
if !durationPtrEqual(g.EvalDelay, expect.EvalDelay) {
t.Fatalf("expected to have eval_delay %v; got %v", expect.EvalDelay, g.EvalDelay)
}
if !boolPtrEqual(g.evalAlignment, expect.evalAlignment) {
t.Fatalf("expected to have eval_alignment %v; got %v", expect.evalAlignment, g.evalAlignment)
}
}
// new rule
@@ -243,37 +237,6 @@ func TestUpdateWith(t *testing.T) {
{Alert: "foo1", Debug: &debug},
},
})
// update group evaluation settings
evalDelay := promutil.NewDuration(time.Minute)
evalAlignment := false
f(config.Group{
Rules: []config.Rule{{
Record: "foo",
Expr: "max(up)",
}},
}, config.Group{
EvalDelay: evalDelay,
EvalAlignment: &evalAlignment,
Rules: []config.Rule{{
Record: "foo",
Expr: "min(up)",
}},
})
}
func durationPtrEqual(a, b *time.Duration) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}
func boolPtrEqual(a, b *bool) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}
func TestUpdateDuringRandSleep(t *testing.T) {

View File

@@ -4,14 +4,12 @@ import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/remotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
)
@@ -120,7 +118,7 @@ func (s *ruleState) add(e StateEntry) {
s.entries[s.cur] = e
}
func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRuleRetryAttempts int, continueWithExecutionErr bool) (int, error) {
func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRuleRetryAttempts int) (int, error) {
var err error
var tss []prompb.TimeSeries
for i := range replayRuleRetryAttempts {
@@ -128,22 +126,6 @@ func replayRule(r Rule, start, end time.Time, rw remotewrite.RWClient, replayRul
if err == nil {
break
}
// retry request if possible to tolerate temporary network or datasource unavailability issues
var esc *httpserver.ErrorWithStatusCode
if errors.As(err, &esc) {
statusCode := esc.StatusCode
// if the status code is 400 or 422, the query failed due to reasons such as an expression syntax error or a resource limit being hit,
// rather than datasource unavailability.
// Continue replaying but skip the problematic execution if continueWithExecutionErr is true, otherwise, return the error without retry.
if statusCode == http.StatusUnprocessableEntity || statusCode == http.StatusBadRequest {
if continueWithExecutionErr {
logger.Errorf("rule %q: %s", r, err)
return 0, nil
} else {
return 0, err
}
}
}
logger.Errorf("attempt %d to execute rule %q failed: %s", i+1, r, err)
time.Sleep(time.Second)
}

View File

@@ -75,13 +75,10 @@ type GroupAlerts struct {
// ApiRule represents a Rule for web view
// see https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md#get-apiv1rules
type ApiRule struct {
// Rule state must be one of these under following scenarios:
// "pending": at least 1 alert in the rule in pending state and no other alert in firing state. (only for alerting rules)
// "firing": at least 1 alert in the rule in firing state. (only for alerting rules)
// "inactive": rule's last evaluation was successful but no alert in the rule in firing or pending state. (only for alerting rules)
// "unhealthy": rule's last evaluation was failed with error. (for both alerting and recording rules)
// "nomatch": rule's last evaluation was successful but no time series matched the rule's expression. (for both alerting and recording rules)
// "ok": the recording rule's last evaluation was successful. (only for recording rules)
// State must be one of these under following scenarios
// "pending": at least 1 alert in the rule in pending state and no other alert in firing ruleState.
// "firing": at least 1 alert in the rule in firing state.
// "inactive": no alert in the rule in firing or pending state.
State string `json:"state"`
Name string `json:"name"`
// Query represents Rule's `expression` field
@@ -240,7 +237,6 @@ func NewAlertAPI(ar *AlertingRule, a *notifier.Alert) *ApiAlert {
}
func (r *ApiRule) ExtendState() {
// if alerting rule already has alerts, then state is already set to either "pending" or "firing" and we don't need to change it
if len(r.Alerts) > 0 {
return
}

View File

@@ -115,7 +115,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
}
WriteRule(w, r, rule)
return true
// used by old vmalert UI
// current used by old vmalert UI and Grafana Alerts
case "/vmalert/groups", "/rules":
rf, err := newRulesFilter(r)
if err != nil {
@@ -128,8 +128,6 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
state = rf.states[0]
rf.states = rf.states[:1]
}
// enable extendedStates by default for vmalert UI
rf.extendedStates = true
lr := rh.groups(rf)
WriteListGroups(w, r, lr.Data.Groups, state)
return true
@@ -545,8 +543,6 @@ func (rh *requestHandler) groups(rf *rulesFilter) *listGroupsResponse {
if !groupFound && !strings.Contains(strings.ToLower(rule.Name), rf.search) {
continue
}
// extendedStates is used by the vmalert UI to extend the rule state with values such as "nomatch" and "unhealthy".
// those states are not supported by Grafana and not part of the Prometheus API spec yet
if rf.extendedStates {
rule.ExtendState()
}

View File

@@ -130,12 +130,9 @@
data-bs-target="#item-{%s g.ID %}"
>
<span class="d-flex gap-2">
{% if g.States["inactive"] > 0 %}<span class="badge bg-light text-success border border-success" title="None of the alert instances is in a pending or firing state">{%d g.States["inactive"] %} inactive</span> {% endif %}
{% if g.States["pending"] > 0 %}<span class="badge bg-warning text-dark" title="At least one alert instance is pending">{%d g.States["pending"] %} pending</span> {% endif %}
{% if g.States["firing"] > 0 %}<span class="badge bg-danger" title="At least one alert instance is firing">{%d g.States["firing"] %} firing</span> {% endif %}
{% if g.States["ok"] > 0 %}<span class="badge bg-success" title="Recording rule last evaluation succeeded">{%d g.States["ok"] %} ok</span>{% endif %}
{% if g.States["unhealthy"] > 0 %}<span class="badge bg-danger" title="Last evaluation failed with an error">{%d g.States["unhealthy"] %} unhealthy</span> {% endif %}
{% if g.States["nomatch"] > 0 %}<span class="badge bg-warning" title="Rule expression matched no time series">{%d g.States["nomatch"] %} nomatch</span> {% endif %}
{% if g.States["unhealthy"] > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.States["unhealthy"] %}</span> {% endif %}
{% if g.States["nomatch"] > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.States["nomatch"] %}</span> {% endif %}
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.States["ok"] %}</span>
</span>
</span>
</span>
@@ -172,7 +169,7 @@
<thead>
<tr>
<th scope="col" class="w-60">Rule</th>
<th scope="col" class="w-20" class="text-center" title="How many series were produced by the rule">Series Returned</th>
<th scope="col" class="w-20" class="text-center" title="How many series were produced by the rule">Series</th>
<th scope="col" class="w-20" class="text-center" title="How many seconds ago rule was executed">Updated</th>
</tr>
</thead>
@@ -191,21 +188,8 @@
{% else %}
<b>record:</b> {%s r.Name %}
{% endif %}
{% if r.State == "inactive" %}
<span><a class="badge bg-success">{%s r.State %}</a></span>
{% elseif r.State == "pending" %}
<span><a class="badge bg-warning">{%s r.State %}</a></span>
{% elseif r.State == "firing" %}
<span><a class="badge bg-danger">{%s r.State %}</a></span>
{% elseif r.State == "ok" %}
<span><a class="badge bg-success">{%s r.State %}</a></span>
{% elseif r.State == "unhealthy" %}
<span><a class="badge bg-danger">{%s r.State %}</a></span>
{% elseif r.State == "nomatch" %}
<span><a class="badge bg-warning">{%s r.State %}</a></span>
{% endif %}
{%= seriesFetchedWarn(prefix, &r) %}
|
{%= seriesFetchedWarn(prefix, &r) %}
<span><a target="_blank" href="{%s prefix+r.WebLink() %}">Details</a></span>
</div>
<div class="col-12">

File diff suppressed because it is too large Load Diff

View File

@@ -96,7 +96,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
initSecretFlags()
buildinfo.Init()
logger.Init()
@@ -387,7 +386,7 @@ func bufferRequestBody(ctx context.Context, r io.ReadCloser, userName string) (i
return nil, &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf("reject request from the user %s because the request body couldn't be read in -maxQueueDuration=%s; read %d bytes in %s",
userName, *maxQueueDuration, len(buf), d.Truncate(time.Second)),
StatusCode: http.StatusRequestTimeout,
StatusCode: http.StatusBadRequest,
}
}
@@ -912,8 +911,3 @@ func slowdownUnauthorizedResponse(r *http.Request) {
}
timerpool.Put(t)
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
pushmetrics.InitSecretFlags()
}

View File

@@ -1,54 +0,0 @@
//go:build synctest
package main
import (
"context"
"net/http"
"testing"
"testing/synctest"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
)
func TestBufferRequestBody_Timeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
defaultMaxQueueDuration := *maxQueueDuration
defer func() {
*maxQueueDuration = defaultMaxQueueDuration
}()
*maxQueueDuration = 100 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), *maxQueueDuration)
defer cancel()
_, err := bufferRequestBody(ctx, &timeoutBody{delay: *maxQueueDuration + 100*time.Millisecond}, "foo")
if err == nil {
t.Fatalf("expecting non-nil error")
}
esc, ok := err.(*httpserver.ErrorWithStatusCode)
if !ok {
t.Fatalf("unexpected error type: %s", err)
}
if esc.StatusCode != http.StatusRequestTimeout {
t.Fatalf("read request body timeout meet unexpected status code; got %d; want %d", esc.StatusCode, http.StatusRequestTimeout)
}
})
}
type timeoutBody struct {
delay time.Duration
}
func (r *timeoutBody) Read(_ []byte) (int, error) {
time.Sleep(r.delay)
return 0, context.DeadlineExceeded
}
func (r *timeoutBody) Close() error {
return nil
}

View File

@@ -47,8 +47,9 @@ func main() {
// Write flags and help message to stdout, since it is easier to grep or pipe.
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
flagutil.RegisterSecretFlag("snapshot.createURL")
flagutil.RegisterSecretFlag("snapshot.deleteURL")
envflag.Parse()
initSecretFlags()
buildinfo.Init()
logger.Init()
@@ -272,10 +273,3 @@ func newRemoteOriginFS(ctx context.Context) (common.RemoteFS, error) {
}
return fs, nil
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
flagutil.RegisterSecretFlag("snapshot.createURL")
flagutil.RegisterSecretFlag("snapshot.deleteURL")
pushmetrics.InitSecretFlags()
}

View File

@@ -47,7 +47,6 @@ func main() {
start := time.Now()
beforeFn := func(c *cli.Context) error {
flag.Parse()
initSecretFlags()
logger.Init()
isSilent = c.Bool(globalSilent)
if c.Bool(globalDisableProgressBar) {
@@ -620,8 +619,3 @@ func initConfigVM(c *cli.Context) (vm.Config, error) {
Backoff: bf,
}, nil
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
pushmetrics.InitSecretFlags()
}

View File

@@ -8,14 +8,12 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/tsdb/chunkenc"
@@ -236,29 +234,9 @@ func processResponse(body io.ReadCloser, callback StreamCallback) error {
// shouldn't be accounted as an error.
for _, res := range readResp.Results {
for _, ts := range res.Timeseries {
// A series contains either float samples or native histogram samples.
// Both fields are processed independently, since a series may switch
// from float to native histogram representation at some point in time,
// so the requested time range may contain samples of both types.
if len(ts.Samples) > 0 {
vmTs := convertSamples(ts.Samples, ts.Labels)
if err := callback(vmTs); err != nil {
return err
}
}
if len(ts.Histograms) > 0 {
hSamples := make([]histogramSample, 0, len(ts.Histograms))
for _, h := range ts.Histograms {
hSamples = append(hSamples, histogramSample{
timestamp: h.Timestamp,
fh: h.ToFloatHistogram(),
})
}
for _, vmTs := range convertHistograms(hSamples, ts.Labels) {
if err := callback(vmTs); err != nil {
return err
}
}
vmTs := convertSamples(ts.Samples, ts.Labels)
if err := callback(vmTs); err != nil {
return err
}
}
}
@@ -285,45 +263,17 @@ func processStreamResponse(body io.ReadCloser, callback StreamCallback) error {
for _, series := range res.ChunkedSeries {
samples := make([]prompb.Sample, 0)
var hSamples []histogramSample
for _, chunk := range series.Chunks {
switch chunk.Type {
case prompb.Chunk_XOR, prompb.Chunk_UNKNOWN:
// In proto3 the `type` field may be left unset (UNKNOWN) for XOR chunks.
// Prometheus remote.proto: "REQUIREMENT: when using proto3, this field
// MUST be set when using anything else than XOR". Senders before native
// histograms support (Prometheus < 2.40) do not set this field at all,
// so UNKNOWN chunks must be parsed as XOR ones.
s, err := parseSamples(chunk.Data)
if err != nil {
return err
}
samples = append(samples, s...)
case prompb.Chunk_HISTOGRAM, prompb.Chunk_FLOAT_HISTOGRAM:
hs, err := parseHistograms(chunk.Type, chunk.Data)
if err != nil {
return err
}
hSamples = append(hSamples, hs...)
default:
return fmt.Errorf("unsupported chunk encoding %q", chunk.Type)
s, err := parseSamples(chunk.Data)
if err != nil {
return err
}
samples = append(samples, s...)
}
// A series contains either XOR chunks or native histogram chunks.
// Both are processed independently, since a series may switch
// from float to native histogram representation at some point in time,
// so the requested time range may contain chunks of both types.
if len(samples) > 0 {
ts := convertSamples(samples, series.Labels)
if err := callback(ts); err != nil {
return err
}
}
for _, ts := range convertHistograms(hSamples, series.Labels) {
if err := callback(ts); err != nil {
return err
}
ts := convertSamples(samples, series.Labels)
if err := callback(ts); err != nil {
return err
}
}
}
@@ -362,151 +312,6 @@ func parseSamples(chunk []byte) ([]prompb.Sample, error) {
return samples, it.Err()
}
// histogramSample represents a single native histogram sample.
type histogramSample struct {
timestamp int64
fh *histogram.FloatHistogram
}
func parseHistograms(encoding prompb.Chunk_Encoding, chunk []byte) ([]histogramSample, error) {
var enc chunkenc.Encoding
switch encoding {
case prompb.Chunk_HISTOGRAM:
enc = chunkenc.EncHistogram
case prompb.Chunk_FLOAT_HISTOGRAM:
enc = chunkenc.EncFloatHistogram
default:
return nil, fmt.Errorf("unsupported histogram chunk encoding %q", encoding)
}
c, err := chunkenc.FromData(enc, chunk)
if err != nil {
return nil, fmt.Errorf("error read chunk: %w", err)
}
var hSamples []histogramSample
it := c.Iterator(nil)
for {
typ := it.Next()
if typ == chunkenc.ValNone {
break
}
switch typ {
case chunkenc.ValHistogram:
ts, h := it.AtHistogram(nil)
hSamples = append(hSamples, histogramSample{
timestamp: ts,
fh: h.ToFloat(nil),
})
case chunkenc.ValFloatHistogram:
ts, fh := it.AtFloatHistogram(nil)
hSamples = append(hSamples, histogramSample{
timestamp: ts,
fh: fh,
})
default:
// Skip unsupported values
continue
}
}
if err := it.Err(); err != nil {
return nil, fmt.Errorf("error iterate over chunks: %w", err)
}
return hSamples, nil
}
// convertHistograms converts native histogram samples into VictoriaMetrics histogram
// time series in the same way as VictoriaMetrics converts native histograms
// received via Prometheus remote write protocol: every native histogram sample
// is converted into `<name>_count` and `<name>_sum` series plus a set of
// `<name>_bucket` series with `vmrange` labels containing non-cumulative bucket counts.
// The only difference is that for native histograms with custom buckets (NHCB)
// bucket bounds are taken from the custom values, while the remote write protocol
// parser ignores custom values and estimates the bounds with the exponential formula.
// See https://prometheus.io/docs/specs/native_histograms/#data-model
func convertHistograms(hSamples []histogramSample, labels []prompb.Label) []*vm.TimeSeries {
if len(hSamples) == 0 {
return nil
}
labelPairs := make([]vm.LabelPair, 0, len(labels))
nameValue := ""
for _, label := range labels {
if label.Name == "__name__" {
nameValue = label.Value
continue
}
labelPairs = append(labelPairs, vm.LabelPair{Name: label.Name, Value: label.Value})
}
// the metric has no name, skip it in the same way as VictoriaMetrics does
// when it receives a native histogram without the metric name via remote write protocol.
if nameValue == "" {
return nil
}
countSeries := &vm.TimeSeries{
Name: nameValue + "_count",
LabelPairs: labelPairs,
}
sumSeries := &vm.TimeSeries{
Name: nameValue + "_sum",
LabelPairs: labelPairs,
}
bucketSeries := make(map[string]*vm.TimeSeries)
// vmranges preserves the order of bucketSeries creation
// in order to get deterministic results.
var vmranges []string
for _, hs := range hSamples {
fh := hs.fh
countSeries.Timestamps = append(countSeries.Timestamps, hs.timestamp)
countSeries.Values = append(countSeries.Values, fh.Count)
sumSeries.Timestamps = append(sumSeries.Timestamps, hs.timestamp)
sumSeries.Values = append(sumSeries.Values, fh.Sum)
it := fh.AllBucketIterator()
for it.Next() {
b := it.At()
if b.Count <= 0 {
continue
}
vmrange := formatVmrange(b.Lower, b.Upper)
s := bucketSeries[vmrange]
if s == nil {
bucketLabelPairs := make([]vm.LabelPair, len(labelPairs), len(labelPairs)+1)
copy(bucketLabelPairs, labelPairs)
bucketLabelPairs = append(bucketLabelPairs, vm.LabelPair{Name: "vmrange", Value: vmrange})
s = &vm.TimeSeries{
Name: nameValue + "_bucket",
LabelPairs: bucketLabelPairs,
}
bucketSeries[vmrange] = s
vmranges = append(vmranges, vmrange)
}
s.Timestamps = append(s.Timestamps, hs.timestamp)
s.Values = append(s.Values, b.Count)
}
}
tss := make([]*vm.TimeSeries, 0, 2+len(vmranges))
tss = append(tss, countSeries, sumSeries)
for _, vmrange := range vmranges {
tss = append(tss, bucketSeries[vmrange])
}
return tss
}
// formatVmrange formats the given bucket bounds into `vmrange` label value
// in the same way as VictoriaMetrics does for native histograms
// received via Prometheus remote write protocol.
func formatVmrange(lower, upper float64) string {
b := make([]byte, 0, 24)
b = strconv.AppendFloat(b, lower, 'e', 3, 64)
b = append(b, "..."...)
b = strconv.AppendFloat(b, upper, 'e', 3, 64)
return string(b)
}
type keyValue struct {
key string
value string

View File

@@ -1,334 +0,0 @@
package remoteread
import (
"bytes"
"io"
"reflect"
"testing"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmctl/vm"
)
func testHistogram(mul int64) *histogram.Histogram {
return &histogram.Histogram{
Schema: 0,
Count: uint64(10 * mul),
Sum: 25.5 * float64(mul),
ZeroThreshold: 0.001,
ZeroCount: uint64(2 * mul),
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
PositiveBuckets: []int64{1 * mul, 2 * mul},
NegativeSpans: []histogram.Span{{Offset: 0, Length: 1}},
NegativeBuckets: []int64{4 * mul},
}
}
func TestConvertHistograms(t *testing.T) {
f := func(hSamples []histogramSample, labels []prompb.Label, expected []*vm.TimeSeries) {
t.Helper()
tss := convertHistograms(hSamples, labels)
if !reflect.DeepEqual(tss, expected) {
t.Fatalf("unexpected result\ngot:\n%v\nwant:\n%v", tss, expected)
}
}
// series without samples
f(nil, []prompb.Label{{Name: "__name__", Value: "foo"}}, nil)
// series without the metric name must be skipped
f([]histogramSample{
{timestamp: 1000, fh: testHistogram(1).ToFloat(nil)},
}, []prompb.Label{{Name: "job", Value: "bar"}}, nil)
// native histogram must be converted to _count, _sum and _bucket series
// in the same way as VictoriaMetrics does for Prometheus remote write protocol
labels := []prompb.Label{
{Name: "__name__", Value: "request_duration_seconds"},
{Name: "job", Value: "bar"},
}
jobLabel := []vm.LabelPair{{Name: "job", Value: "bar"}}
bucketLabels := func(vmrange string) []vm.LabelPair {
return []vm.LabelPair{
{Name: "job", Value: "bar"},
{Name: "vmrange", Value: vmrange},
}
}
f([]histogramSample{
{timestamp: 1000, fh: testHistogram(1).ToFloat(nil)},
{timestamp: 2000, fh: testHistogram(2).ToFloat(nil)},
}, labels, []*vm.TimeSeries{
{
Name: "request_duration_seconds_count",
LabelPairs: jobLabel,
Timestamps: []int64{1000, 2000},
Values: []float64{10, 20},
},
{
Name: "request_duration_seconds_sum",
LabelPairs: jobLabel,
Timestamps: []int64{1000, 2000},
Values: []float64{25.5, 51},
},
{
Name: "request_duration_seconds_bucket",
LabelPairs: bucketLabels("-1.000e+00...-5.000e-01"),
Timestamps: []int64{1000, 2000},
Values: []float64{4, 8},
},
{
Name: "request_duration_seconds_bucket",
LabelPairs: bucketLabels("-1.000e-03...1.000e-03"),
Timestamps: []int64{1000, 2000},
Values: []float64{2, 4},
},
{
Name: "request_duration_seconds_bucket",
LabelPairs: bucketLabels("5.000e-01...1.000e+00"),
Timestamps: []int64{1000, 2000},
Values: []float64{1, 2},
},
{
Name: "request_duration_seconds_bucket",
LabelPairs: bucketLabels("1.000e+00...2.000e+00"),
Timestamps: []int64{1000, 2000},
Values: []float64{3, 6},
},
})
}
func TestParseHistograms(t *testing.T) {
c := chunkenc.NewHistogramChunk()
app, err := c.Appender()
if err != nil {
t.Fatalf("cannot create chunk appender: %s", err)
}
if _, _, _, err := app.AppendHistogram(nil, 0, 1000, testHistogram(1), true); err != nil {
t.Fatalf("cannot append histogram: %s", err)
}
if _, _, _, err := app.AppendHistogram(nil, 0, 2000, testHistogram(2), true); err != nil {
t.Fatalf("cannot append histogram: %s", err)
}
hSamples, err := parseHistograms(prompb.Chunk_HISTOGRAM, c.Bytes())
if err != nil {
t.Fatalf("cannot parse histogram chunk: %s", err)
}
if len(hSamples) != 2 {
t.Fatalf("unexpected number of histogram samples; got %d; want 2", len(hSamples))
}
for i, expected := range []struct {
timestamp int64
count float64
sum float64
}{
{timestamp: 1000, count: 10, sum: 25.5},
{timestamp: 2000, count: 20, sum: 51},
} {
if hSamples[i].timestamp != expected.timestamp {
t.Fatalf("unexpected timestamp; got %d; want %d", hSamples[i].timestamp, expected.timestamp)
}
if hSamples[i].fh.Count != expected.count {
t.Fatalf("unexpected count; got %f; want %f", hSamples[i].fh.Count, expected.count)
}
if hSamples[i].fh.Sum != expected.sum {
t.Fatalf("unexpected sum; got %f; want %f", hSamples[i].fh.Sum, expected.sum)
}
}
// unsupported chunk encoding must return error
if _, err := parseHistograms(prompb.Chunk_XOR, c.Bytes()); err == nil {
t.Fatalf("expecting non-nil error for unsupported chunk encoding")
}
}
func TestProcessResponse(t *testing.T) {
readResp := &prompb.ReadResponse{
Results: []*prompb.QueryResult{
{
Timeseries: []*prompb.TimeSeries{
{
Labels: []prompb.Label{
{Name: "__name__", Value: "cpu_usage"},
{Name: "job", Value: "bar"},
},
Samples: []prompb.Sample{
{Timestamp: 1000, Value: 1.5},
},
},
{
Labels: []prompb.Label{
{Name: "__name__", Value: "request_duration_seconds"},
{Name: "job", Value: "bar"},
},
Histograms: []prompb.Histogram{
prompb.FromIntHistogram(1000, testHistogram(1)),
},
},
},
},
},
}
data, err := proto.Marshal(readResp)
if err != nil {
t.Fatalf("cannot marshal ReadResponse: %s", err)
}
compressed := snappy.Encode(nil, data)
var tss []*vm.TimeSeries
err = processResponse(io.NopCloser(bytes.NewReader(compressed)), func(ts *vm.TimeSeries) error {
tss = append(tss, ts)
return nil
})
if err != nil {
t.Fatalf("cannot process response: %s", err)
}
// 1 float series + _count + _sum + 4 buckets
if len(tss) != 7 {
t.Fatalf("unexpected number of time series; got %d; want 7", len(tss))
}
if tss[0].Name != "cpu_usage" || !reflect.DeepEqual(tss[0].Values, []float64{1.5}) {
t.Fatalf("unexpected float series: %v", tss[0])
}
if tss[1].Name != "request_duration_seconds_count" || !reflect.DeepEqual(tss[1].Values, []float64{10}) {
t.Fatalf("unexpected _count series: %v", tss[1])
}
if tss[2].Name != "request_duration_seconds_sum" || !reflect.DeepEqual(tss[2].Values, []float64{25.5}) {
t.Fatalf("unexpected _sum series: %v", tss[2])
}
for _, ts := range tss[3:] {
if ts.Name != "request_duration_seconds_bucket" {
t.Fatalf("unexpected bucket series name %q", ts.Name)
}
}
}
type nopFlusher struct{}
func (nopFlusher) Flush() {}
func TestProcessStreamResponse(t *testing.T) {
// build a histogram chunk
hc := chunkenc.NewHistogramChunk()
hApp, err := hc.Appender()
if err != nil {
t.Fatalf("cannot create histogram chunk appender: %s", err)
}
if _, _, _, err := hApp.AppendHistogram(nil, 0, 1000, testHistogram(1), true); err != nil {
t.Fatalf("cannot append histogram: %s", err)
}
// build a float chunk
xc := chunkenc.NewXORChunk()
xApp, err := xc.Appender()
if err != nil {
t.Fatalf("cannot create xor chunk appender: %s", err)
}
xApp.Append(0, 1000, 1.5)
res := &prompb.ChunkedReadResponse{
ChunkedSeries: []*prompb.ChunkedSeries{
{
Labels: []prompb.Label{
{Name: "__name__", Value: "request_duration_seconds"},
{Name: "job", Value: "bar"},
},
Chunks: []prompb.Chunk{
{Type: prompb.Chunk_HISTOGRAM, Data: hc.Bytes()},
},
},
{
Labels: []prompb.Label{
{Name: "__name__", Value: "cpu_usage"},
},
Chunks: []prompb.Chunk{
{Type: prompb.Chunk_XOR, Data: xc.Bytes()},
},
},
{
Labels: []prompb.Label{
{Name: "__name__", Value: "memory_usage"},
},
Chunks: []prompb.Chunk{
// the `type` field may be unset for XOR chunks,
// such chunks must be parsed as XOR ones
{Type: prompb.Chunk_UNKNOWN, Data: xc.Bytes()},
},
},
},
}
data, err := proto.Marshal(res)
if err != nil {
t.Fatalf("cannot marshal ChunkedReadResponse: %s", err)
}
var buf bytes.Buffer
cw := remote.NewChunkedWriter(&buf, nopFlusher{})
if _, err := cw.Write(data); err != nil {
t.Fatalf("cannot write chunked response: %s", err)
}
var tss []*vm.TimeSeries
err = processStreamResponse(io.NopCloser(&buf), func(ts *vm.TimeSeries) error {
tss = append(tss, ts)
return nil
})
if err != nil {
t.Fatalf("cannot process stream response: %s", err)
}
// _count + _sum + 4 buckets + 1 float series + 1 float series from UNKNOWN chunk
if len(tss) != 8 {
t.Fatalf("unexpected number of time series; got %d; want 8", len(tss))
}
if tss[0].Name != "request_duration_seconds_count" || !reflect.DeepEqual(tss[0].Values, []float64{10}) {
t.Fatalf("unexpected _count series: %v", tss[0])
}
if tss[1].Name != "request_duration_seconds_sum" || !reflect.DeepEqual(tss[1].Values, []float64{25.5}) {
t.Fatalf("unexpected _sum series: %v", tss[1])
}
for _, ts := range tss[2:6] {
if ts.Name != "request_duration_seconds_bucket" {
t.Fatalf("unexpected bucket series name %q", ts.Name)
}
}
if tss[6].Name != "cpu_usage" || !reflect.DeepEqual(tss[6].Values, []float64{1.5}) {
t.Fatalf("unexpected float series: %v", tss[6])
}
if tss[7].Name != "memory_usage" || !reflect.DeepEqual(tss[7].Values, []float64{1.5}) {
t.Fatalf("unexpected float series from UNKNOWN chunk: %v", tss[7])
}
}
func TestParseFloatHistograms(t *testing.T) {
c := chunkenc.NewFloatHistogramChunk()
app, err := c.Appender()
if err != nil {
t.Fatalf("cannot create chunk appender: %s", err)
}
fh := testHistogram(1).ToFloat(nil)
if _, _, _, err := app.AppendFloatHistogram(nil, 0, 1000, fh, true); err != nil {
t.Fatalf("cannot append float histogram: %s", err)
}
hSamples, err := parseHistograms(prompb.Chunk_FLOAT_HISTOGRAM, c.Bytes())
if err != nil {
t.Fatalf("cannot parse float histogram chunk: %s", err)
}
if len(hSamples) != 1 {
t.Fatalf("unexpected number of histogram samples; got %d; want 1", len(hSamples))
}
if hSamples[0].timestamp != 1000 {
t.Fatalf("unexpected timestamp; got %d; want 1000", hSamples[0].timestamp)
}
if hSamples[0].fh.Count != 10 {
t.Fatalf("unexpected count; got %f; want 10", hSamples[0].fh.Count)
}
}

View File

@@ -38,7 +38,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
initSecretFlags()
buildinfo.Init()
logger.Init()
@@ -113,8 +112,3 @@ func newSrcFS(ctx context.Context) (common.RemoteFS, error) {
}
return fs, nil
}
// initSecretFlags manages the secret flags for this app and must be called after flag parsing and before logger init.
func initSecretFlags() {
pushmetrics.InitSecretFlags()
}

View File

@@ -59,12 +59,6 @@ func Init(vmselectMaxConcurrentRequests int, vmselectMaxQueueDuration time.Durat
initVMUIConfig()
vmalertproxy.Init(*vmalertProxyURL)
}
// InitSecretFlags manages the secret flags for this pkg and must be called by app-level initSecretFlags.
// It should run before logger initialization and package Init() (if exists).
func InitSecretFlags() {
flagutil.RegisterSecretFlag("vmalert.proxyURL")
}
@@ -375,10 +369,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
}
return true
case "/tags/delSeries":
if r.Method != "POST" {
http.Error(w, fmt.Sprintf("Only POST method is allowed. Got %s.", r.Method), http.StatusMethodNotAllowed)
return true
}
if !httpserver.CheckAuthFlag(w, r, deleteAuthKey) {
return true
}
@@ -398,10 +388,6 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
}
return true
case "/api/v1/admin/tsdb/delete_series":
if r.Method != "POST" {
http.Error(w, fmt.Sprintf("Only POST method is allowed. Got %s.", r.Method), http.StatusMethodNotAllowed)
return true
}
if !httpserver.CheckAuthFlag(w, r, deleteAuthKey) {
return true
}
@@ -555,7 +541,7 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
return true
case "/metric-relabel-debug":
promscrapeMetricRelabelDebugRequests.Inc()
promscrape.WriteMetricRelabelDebug(w, r, "", nil)
promscrape.WriteMetricRelabelDebug(w, r)
return true
case "/target-relabel-debug":
promscrapeTargetRelabelDebugRequests.Inc()

View File

@@ -516,7 +516,7 @@ func DeleteHandler(startTime time.Time, r *http.Request) error {
cp.deadline = searchutil.GetDeadlineForDelete(r, startTime)
if !cp.IsDefaultTimeRange() {
return fmt.Errorf("delete API does not support specific time ranges using start and end args, the series can only be deleted completely")
return fmt.Errorf("start=%d and end=%d args aren't supported. Remove these args from the query in order to delete all the matching metrics", cp.start, cp.end)
}
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxDeleteSeries)
deletedCount, err := netstorage.DeleteSeries(nil, sq, cp.deadline)
@@ -540,11 +540,11 @@ func LabelValuesHandler(qt *querytracer.Tracer, startTime time.Time, labelName s
cp, err := getCommonParamsForLabelsAPI(r, startTime, false)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
limit, err := httputil.GetInt(r, "limit")
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
@@ -584,7 +584,7 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
cp, err := getCommonParams(r, startTime, false)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
cp.deadline = searchutil.GetDeadlineForStatusRequest(r, startTime)
@@ -596,7 +596,7 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
} else {
t, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `date` arg %q: %w", dateStr, err))
return fmt.Errorf("cannot parse `date` arg %q: %w", dateStr, err)
}
date = uint64(t.Unix()) / secsPerDay
}
@@ -607,7 +607,7 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
if len(topNStr) > 0 {
n, err := strconv.Atoi(topNStr)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err))
return fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err)
}
if n <= 0 {
n = 1
@@ -645,11 +645,11 @@ func LabelsHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
cp, err := getCommonParamsForLabelsAPI(r, startTime, false)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
limit, err := httputil.GetInt(r, "limit")
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxLabelsAPISeries)
labels, err := netstorage.LabelNames(qt, sq, limit, cp.deadline)
@@ -671,9 +671,10 @@ func LabelsHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
//
// See https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metric-metadata
func MetadataHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWriter, r *http.Request) error {
limit, err := httputil.GetInt(r, "limit")
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
if limit < 0 {
limit = 0
@@ -733,11 +734,11 @@ func SeriesHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseW
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/91
cp, err := getCommonParamsForLabelsAPI(r, startTime, true)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
limit, err := httputil.GetInt(r, "limit")
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
sq := storage.NewSearchQuery(cp.start, cp.end, cp.filterss, *maxSeriesLimit)
@@ -771,19 +772,19 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
mayCache := !httputil.GetBool(r, "nocache")
query := r.FormValue("query")
if len(query) == 0 {
return httpserver.InvalidParamError(fmt.Errorf("missing `query` arg"))
return fmt.Errorf("missing `query` arg")
}
start, err := httputil.GetTime(r, "time", ct)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
lookbackDelta, err := getMaxLookback(r)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
step, err := httputil.GetDuration(r, "step", lookbackDelta)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
if step <= 0 {
step = defaultStep
@@ -791,16 +792,16 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return httpserver.InvalidParamError(fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen))
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
}
etfs, err := searchutil.GetExtraTagFilters(r)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
if childQuery, windowExpr, offsetExpr := promql.IsMetricSelectorWithRollup(query); childQuery != "" {
window, err := windowExpr.NonNegativeDuration(step)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err))
return fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err)
}
offset := offsetExpr.Duration(step)
start -= offset
@@ -814,7 +815,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
tagFilterss, err := getTagFilterssFromMatches([]string{childQuery})
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
filterss := searchutil.JoinTagFilterss(tagFilterss, etfs)
@@ -830,25 +831,22 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
return nil
}
if childQuery, windowExpr, stepExpr, offsetExpr := promql.IsRollup(query); childQuery != "" {
if len(childQuery) > maxLen {
return httpserver.InvalidParamError(fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(childQuery), maxLen))
}
newStep, err := stepExpr.NonNegativeDuration(step)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse step in square brackets at %s: %w", query, err))
return fmt.Errorf("cannot parse step in square brackets at %s: %w", query, err)
}
if newStep > 0 {
step = newStep
}
window, err := windowExpr.NonNegativeDuration(step)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err))
return fmt.Errorf("cannot parse lookbehind window in square brackets at %s: %w", query, err)
}
offset := offsetExpr.Duration(step)
start -= offset
end := start
start = end - window
if err := queryRangeHandler(qt, startTime, w, childQuery, start, end, step, lookbackDelta, r, ct, etfs); err != nil {
if err := queryRangeHandler(qt, startTime, w, childQuery, start, end, step, r, ct, etfs); err != nil {
return fmt.Errorf("error when executing query=%q on the time range (start=%d, end=%d, step=%d): %w", childQuery, start, end, step, err)
}
return nil
@@ -856,7 +854,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
queryOffset, err := getLatencyOffsetMilliseconds(r)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
if !httputil.GetBool(r, "nocache") && ct-start < queryOffset && start-ct < queryOffset {
// Adjust start time only if `nocache` arg isn't set.
@@ -930,43 +928,45 @@ func QueryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
ct := startTime.UnixNano() / 1e6
query := r.FormValue("query")
if len(query) == 0 {
return httpserver.InvalidParamError(fmt.Errorf("missing `query` arg"))
}
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return httpserver.InvalidParamError(fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen))
return fmt.Errorf("missing `query` arg")
}
start, err := httputil.GetTime(r, "start", ct-defaultStep)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
end, err := httputil.GetTime(r, "end", ct)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
step, err := httputil.GetDuration(r, "step", defaultStep)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
etfs, err := searchutil.GetExtraTagFilters(r)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
lookbackDelta, err := getMaxLookback(r)
if err != nil {
return httpserver.InvalidParamError(err)
}
if err := queryRangeHandler(qt, startTime, w, query, start, end, step, lookbackDelta, r, ct, etfs); err != nil {
if err := queryRangeHandler(qt, startTime, w, query, start, end, step, r, ct, etfs); err != nil {
return fmt.Errorf("error when executing query=%q on the time range (start=%d, end=%d, step=%d): %w", query, start, end, step, err)
}
return nil
}
func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWriter, query string,
start, end, step, lookbackDelta int64, r *http.Request, ct int64, etfs [][]storage.TagFilter) error {
start, end, step int64, r *http.Request, ct int64, etfs [][]storage.TagFilter) error {
deadline := searchutil.GetDeadlineForQuery(r, startTime)
mayCache := !httputil.GetBool(r, "nocache")
optimizeRepeatedBinaryOpSubexprs := httputil.GetBool(r, "optimize_repeated_binary_op_subexprs")
lookbackDelta, err := getMaxLookback(r)
if err != nil {
return err
}
// Validate input args.
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
}
if start > end {
end = start + defaultStep
}
@@ -1005,7 +1005,7 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
if step < maxStepForPointsAdjustment.Milliseconds() {
queryOffset, err := getLatencyOffsetMilliseconds(r)
if err != nil {
return httpserver.InvalidParamError(err)
return err
}
if ct-queryOffset < end {
result = adjustLastPoints(result, ct-queryOffset, ct+step)
@@ -1156,13 +1156,13 @@ func QueryStatsHandler(w http.ResponseWriter, r *http.Request) error {
if len(topNStr) > 0 {
n, err := strconv.Atoi(topNStr)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err))
return fmt.Errorf("cannot parse `topN` arg %q: %w", topNStr, err)
}
topN = n
}
maxLifetimeMsecs, err := httputil.GetDuration(r, "maxLifetime", 10*60*1000)
if err != nil {
return httpserver.InvalidParamError(fmt.Errorf("cannot parse `maxLifetime` arg: %w", err))
return fmt.Errorf("cannot parse `maxLifetime` arg: %w", err)
}
maxLifetime := time.Duration(maxLifetimeMsecs) * time.Millisecond
w.Header().Set("Content-Type", "application/json")

View File

@@ -164,11 +164,11 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
left := bfa.left
right := bfa.right
op := bfa.be.Op
isCmpOp := metricsql.IsBinaryOpCmp(op)
if !isCmpOp {
// Do not remove empty series for comparison operations, since NaN can be an
// explicitly present sample value. In particular, `value != NaN` is true.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/150.
switch true {
case metricsql.IsBinaryOpCmp(op):
// Do not remove empty series for comparison operations,
// since this may lead to missing result.
default:
left = removeEmptySeries(left)
right = removeEmptySeries(right)
}
@@ -191,14 +191,6 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
isBool := bfa.be.Bool
fillLeft := bfa.be.FillLeft
fillRight := bfa.be.FillRight
// A NaN produced by a vector comparison denotes a filtered-out sample rather
// than an explicitly present NaN value. Drop it when this result is used as
// the right-hand side of another comparison.
// Note that filtered-out samples surviving as NaN inside other wrappers such as
// transform functions or arithmetic operations aren't detected, since such NaN
// is indistinguishable from an explicitly present NaN value there.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10018.
dropNaNRight := isCmpOp && isVectorComparisonExpr(bfa.be.Right)
for i, tsLeft := range left {
leftValues := tsLeft.Values
rightValues := right[i].Values
@@ -211,16 +203,11 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
b := rightValues[j]
leftIsNaN := math.IsNaN(a)
rightIsNaN := math.IsNaN(b)
// Both sides are NaN.
// apply the fill value when either the left or right side is NaN, but not both.
if leftIsNaN && rightIsNaN {
dstValues[j] = bf(a, b, isBool)
continue
}
if dropNaNRight && rightIsNaN && fillRight == nil {
dstValues[j] = nan
continue
}
// Apply the fill value when either the left or right side is NaN, but not both.
if leftIsNaN && fillLeft != nil {
a = fillLeft.N
}
@@ -236,38 +223,6 @@ func newBinaryOpFunc(bf func(left, right float64, isBool bool) float64) binaryOp
}
}
// isVectorComparisonExpr returns whether e is a comparison operation
// with at least one non-scalar operand.
func isVectorComparisonExpr(e metricsql.Expr) bool {
if re, ok := e.(*metricsql.RollupExpr); ok && re.Window == nil {
// Unwrap `(...) offset <d>` and `(...) @ <t>`, which do not change
// the shape of the result. Subqueries are left as is, since rollup
// functions skip NaN values on their own.
e = re.Expr
}
be, ok := e.(*metricsql.BinaryOpExpr)
if !ok || !metricsql.IsBinaryOpCmp(be.Op) {
return false
}
return !isScalarLikeExpr(be.Left) || !isScalarLikeExpr(be.Right)
}
// isScalarLikeExpr returns whether e always evaluates to a scalar value.
func isScalarLikeExpr(e metricsql.Expr) bool {
switch e := e.(type) {
case *metricsql.NumberExpr, *metricsql.DurationExpr:
return true
case *metricsql.BinaryOpExpr:
return isScalarLikeExpr(e.Left) && isScalarLikeExpr(e.Right)
case *metricsql.FuncExpr:
switch strings.ToLower(e.Name) {
case "now", "pi", "scalar", "start", "end", "step", "time", "timezone_offset":
return true
}
}
return false
}
func adjustBinaryOpTags(be *metricsql.BinaryOpExpr, left, right []*timeseries) ([]*timeseries, []*timeseries, []*timeseries, error) {
if len(be.GroupModifier.Op) == 0 && len(be.JoinModifier.Op) == 0 {
if isScalar(left) {

View File

@@ -11,7 +11,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/decimal"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
@@ -47,15 +46,15 @@ func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly boo
e, err := parsePromQLWithCache(q)
if err != nil {
return nil, httpserver.InvalidParamError(err)
return nil, err
}
if *disableImplicitConversion || *logImplicitConversion {
isInvalid := metricsql.IsLikelyInvalid(e)
if isInvalid && *disableImplicitConversion {
// we don't add query=%q to err message as it will be added by the caller
return nil, httpserver.InvalidParamError(fmt.Errorf("query requires implicit conversion and is rejected according to -search.disableImplicitConversion command-line flag. " +
"See https://docs.victoriametrics.com/victoriametrics/metricsql/#implicit-query-conversions for details"))
return nil, fmt.Errorf("query requires implicit conversion and is rejected according to -search.disableImplicitConversion command-line flag. " +
"See https://docs.victoriametrics.com/victoriametrics/metricsql/#implicit-query-conversions for details")
}
if isInvalid && *logImplicitConversion {
logger.Warnf("query=%q requires implicit conversion, see https://docs.victoriametrics.com/victoriametrics/metricsql/#implicit-query-conversions for details", e.AppendString(nil))

View File

@@ -2994,136 +2994,6 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_nan_left_vector_right_scalar`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != NaN`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`compare_to_non_nan_scalar_right`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != 1200`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, nan, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`compare_to_nan_vector_right`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != label_set(NaN, "foo", "bar")`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`compare_to_nan_scalar_comparison_right`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != (1 > 2)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`compare_to_empty_vector_right`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != (label_set(time(), "foo", "bar") > 100000)`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_empty_vector_right_offset`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != ((label_set(time(), "foo", "bar") > 100000) offset 0s)`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_empty_vector_left`, func(t *testing.T) {
// A missing sample on the left side results in NaN for every comparison
// operation, so no special handling is needed there.
t.Parallel()
q := `(label_set(time(), "foo", "bar") > 100000) != label_set(time(), "foo", "bar")`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_empty_series_right_bool`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") == bool (label_set(time(), "foo", "bar") > 100000)`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_partially_empty_series_right`, func(t *testing.T) {
// Prometheus drops the timestamps filtered out by the comparison on the right.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/11100#issuecomment-4933952390.
t.Parallel()
q := `label_set(time(), "foo", "bar") != (label_set(time(), "foo", "bar") * 2 > 2800)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{nan, nan, nan, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`compare_to_empty_unlabeled_vector_right`, func(t *testing.T) {
t.Parallel()
q := `sum(label_set(time(), "foo", "bar")) != (sum(label_set(time(), "foo", "bar")) > 100000)`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_empty_series_right_with_fill_left`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != fill_left(0) (label_set(time(), "foo", "bar") > 100000)`
resultExpected := []netstorage.Result{}
f(q, resultExpected)
})
t.Run(`compare_to_empty_series_right_with_fill_right`, func(t *testing.T) {
t.Parallel()
q := `label_set(time(), "foo", "bar") != fill_right(0) (label_set(time(), "foo", "bar") > 100000)`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
r.MetricName.Tags = []storage.Tag{{
Key: []byte("foo"),
Value: []byte("bar"),
}}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run(`-1 < 2`, func(t *testing.T) {
t.Parallel()
q := `-1 < 2`

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t,n)=>()=>{if(n)throw n[0];try{return e&&(t=e(e=0)),t}catch(e){throw n=[e],e}},s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},l=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},u=(n,r,a)=>(a=n==null?{}:e(i(n)),l(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),d=e=>a.call(e,`module.exports`)?e[`module.exports`]:l(t({},`__esModule`,{value:!0}),e);export{u as a,d as i,o as n,c as r,s as t};

View File

@@ -0,0 +1 @@
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(e&&(t=e(e=0)),t),s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},l=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},u=(n,r,a)=>(a=n==null?{}:e(i(n)),l(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),d=e=>a.call(e,`module.exports`)?e[`module.exports`]:l(t({},`__esModule`,{value:!0}),e);export{u as a,d as i,o as n,c as r,s as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,11 +37,11 @@
<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-B1dXK3k7.js"></script>
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-CNC7AqOf.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-DwJYpOdw.js">
<script type="module" crossorigin src="./assets/index-xYKUiOTH.js"></script>
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-Cyuzqnbw.js">
<link rel="modulepreload" crossorigin href="./assets/vendor-B83wxFqK.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-CnsZ1jie.css">
<link rel="stylesheet" crossorigin href="./assets/index-BJqoElx2.css">
<link rel="stylesheet" crossorigin href="./assets/index-BBUnmLOr.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -35,10 +35,7 @@ var (
"By default, or when set to 0, -maxBackfillAge equals to -retentionPeriod, e.g. it is unlimited within the configured retention. "+
"This can be useful for limiting ingestion of historical samples, for example, when older data has been moved to another storage tier. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#retention")
vmselectAddr = flag.String("vmselectAddr", "", "TCP address to listen for incoming connections from vmselect. "+
"When set, the node will be able to accept cluster-native vmselect RPC requests as if it were vmstorage. "+
"The tenant ID assigned to this node's data is controlled by -accountID and -projectID flags. "+
"See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#multi-tenancy")
vmselectAddr = flag.String("vmselectAddr", "", "TCP address to accept connections from vmselect services")
vmselectDisableRPCCompression = flag.Bool("rpc.disableCompression", false, "Whether to disable compression of the data sent from vmstorage to vmselect. "+
"This reduces CPU usage at the cost of higher network bandwidth usage")
snapshotAuthKey = flagutil.NewPassword("snapshotAuthKey", "authKey, which must be passed in query string to /snapshot* pages. It overrides -httpAuth.*")

View File

@@ -1,4 +1,4 @@
FROM golang:1.26.5 AS build-web-stage
FROM golang:1.26.4 AS build-web-stage
COPY build /build
WORKDIR /build

View File

@@ -1,5 +0,0 @@
<svg width="48" height="48" fill="#020202" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0"/>
<path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z"/>
<path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<link id="favicon" rel="icon" href="/assets/favicon.svg" />
<link rel="apple-touch-icon" href="/assets/favicon.svg" />
<link id="mask-icon" rel="mask-icon" href="/assets/favicon.svg?no-inline" color="#000000">
<link rel="icon" href="/favicon.svg"/>
<link rel="apple-touch-icon" href="/favicon.svg"/>
<link rel="mask-icon" href="/favicon.svg" color="#000000">
<meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"start": "vite",
"start:playground": "cross-env PLAYGROUND=true npm run start",
"build": "vite build",
"lint": "eslint --format stylish 'src/**/*.{ts,tsx}'",
"lint": "eslint --output-file vmui-lint-report.json --format json 'src/**/*.{ts,tsx}'",
"lint:local": "eslint --ext .ts,.tsx -f stylish src",
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
"copy-metricsql-docs": "cp ../../../../docs/MetricsQL.md src/assets/MetricsQL.md || true",
@@ -23,40 +23,40 @@
"classnames": "^2.5.1",
"dayjs": "^1.11.21",
"lodash.debounce": "^4.0.8",
"marked": "^18.0.6",
"preact": "^10.29.7",
"qs": "^6.15.3",
"marked": "^18.0.5",
"preact": "^10.29.2",
"qs": "^6.15.2",
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.18.1",
"react-router-dom": "^7.17.0",
"uplot": "^1.6.32",
"vite": "^8.1.4",
"vite": "^8.0.16",
"web-vitals": "^5.3.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.6",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.2",
"@preact/preset-vite": "^2.10.5",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/preact": "^3.2.4",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^26.1.1",
"@types/node": "^25.9.2",
"@types/qs": "^6.15.1",
"@types/react": "^19.2.17",
"@types/react-input-mask": "^3.0.6",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/eslint-plugin": "^8.64.0",
"@typescript-eslint/parser": "^8.64.0",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.7.0",
"http-proxy-middleware": "^4.2.0",
"globals": "^17.6.0",
"http-proxy-middleware": "^4.1.0",
"jsdom": "^29.1.1",
"postcss": "^8.5.19",
"postcss": "^8.5.15",
"sass-embedded": "^1.100.0",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
"vitest": "^4.1.8"
},
"browserslist": {
"production": [

View File

@@ -0,0 +1 @@
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24.5475 0C10.3246.0265251 1.11379 3.06365 4.40623 6.10077c0 0 12.32997 11.23333 16.58217 14.84083.8131.6896 2.1728 1.1936 3.5191 1.2201h.1199c1.3463-.0265 2.706-.5305 3.5191-1.2201 4.2522-3.5942 16.5422-14.84083 16.5422-14.84083C48.0478 3.06365 38.8636.0265251 24.6674 0" fill="#020202"/><path d="M28.1579 27.0159c-.8131.6896-2.1728 1.1936-3.5191 1.2201h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2201-2.9725-2.5067-13.35639-11.87-17.26201-15.3979v5.4112c0 .5968.22661 1.3793.6265 1.7506C7.00358 21.1936 17.2675 30.5437 20.9731 33.6737c.8132.6896 2.1728 1.1936 3.5191 1.2201h.12c1.3463-.0265 2.7059-.5305 3.519-1.2201 3.679-3.13 13.9429-12.4536 16.6089-14.8939.4132-.3713.6265-1.1538.6265-1.7506V11.618c-3.9323 3.5411-14.3162 12.931-17.2354 15.3979h.0267Z" fill="#020202"/><path d="M28.1579 39.748c-.8131.6897-2.1728 1.1937-3.5191 1.2202h-.12c-1.3463-.0265-2.7059-.5305-3.519-1.2202-2.9725-2.4933-13.35639-11.8567-17.26201-15.3978v5.4111c0 .5969.22661 1.3793.6265 1.7507C7.00358 33.9258 17.2675 43.2759 20.9731 46.4058c.8132.6897 2.1728 1.1937 3.5191 1.2202h.12c1.3463-.0265 2.7059-.5305 3.519-1.2202 3.679-3.1299 13.9429-12.4535 16.6089-14.8938.4132-.3714.6265-1.1538.6265-1.7507v-5.4111c-3.9323 3.5411-14.3162 12.931-17.2354 15.3978h.0267Z" fill="#020202"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -3,7 +3,7 @@
"name": "vmui",
"icons": [
{
"src": "./assets/favicon.svg",
"src": "favicon.svg",
"sizes": "any",
"type": "image/svg+xml"
}

View File

@@ -1,61 +0,0 @@
import { FC, useMemo } from "preact/compat";
import "./style.scss";
import { createFaviconUrl } from "../../../../utils/favicon";
import classNames from "classnames";
import { useBrowserTabSync } from "./hooks/useBrowserTabSync";
import { CloseIcon } from "../../../Main/Icons";
import { faviconColors } from "../../../../constants/faviconColors";
const BrowserTabController: FC = () => {
const { faviconColor, changeFaviconColor } = useBrowserTabSync();
const faviconUrl = useMemo(() => {
return createFaviconUrl(faviconColor);
}, [faviconColor]);
const createHandlerClick = (color?: string) => () => {
changeFaviconColor(color);
};
return (
<div className="vm-browser-tab-controller">
<p className="vm-server-configurator__title">Favicon color</p>
<div className="vm-browser-tab-controller-palette">
<div className="vm-browser-tab-controller-palette-list">
<button
className="vm-browser-tab-controller-palette-list__item vm-browser-tab-controller-palette-list__item_reset"
type="button"
onClick={createHandlerClick()}
aria-label="Reset favicon color"
>
<CloseIcon />
</button>
{faviconColors.map(color => (
<button
className={classNames({
"vm-browser-tab-controller-palette-list__item": true,
"vm-browser-tab-controller-palette-list__item_selected": faviconColor === color
})}
key={color}
type="button"
style={{ color }}
onClick={createHandlerClick(color)}
aria-label={`Set favicon color to ${color}`}
aria-pressed={faviconColor === color}
/>
))}
</div>
<img
className="vm-browser-tab-controller-palette__preview"
src={faviconUrl}
alt="Favicon preview"
/>
</div>
</div>
);
};
export default BrowserTabController;

View File

@@ -1,41 +0,0 @@
import useEventListener from "../../../../../hooks/useEventListener";
import { useEffect, useState } from "preact/compat";
import { getFromStorage, removeFromStorage, saveToStorage } from "../../../../../utils/storage";
import { getFaviconStorageKey, updateFaviconColor } from "../../../../../utils/favicon";
const storageKey = `FAVICON_COLOR:${getFaviconStorageKey()}` as const;
const getColorFromStorage = () => {
return getFromStorage(storageKey) as string | undefined;
};
export const useBrowserTabSync = () => {
const [faviconColor, setFaviconColor] = useState(getColorFromStorage);
const handleUpdateColor = () => {
setFaviconColor(getColorFromStorage());
};
const changeFaviconColor = (color?: string) => {
if (color) {
saveToStorage(storageKey, color);
} else {
removeFromStorage([storageKey]);
}
};
useEffect(() => {
handleUpdateColor();
}, []);
useEffect(() => {
updateFaviconColor(faviconColor);
}, [faviconColor]);
useEventListener("storage", handleUpdateColor);
return {
faviconColor,
changeFaviconColor,
};
};

View File

@@ -1,71 +0,0 @@
@use "src/styles/variables" as *;
$color-item-size: 28px;
$outline-width: 2px;
$outline-offset: 2px;
$outline-space: $outline-width + $outline-offset;
.vm-browser-tab-controller {
.vm-server-configurator__title {
padding-bottom: 2px;
}
&-palette {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: calc($padding-large * 2);
&-list {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: $padding-small;
&__item {
width: $color-item-size;
height: $color-item-size;
aspect-ratio: 1;
border-radius: 50%;
background-color: currentColor;
cursor: pointer;
transition-property: transform;
transition-duration: 0.15s;
transition-timing-function: linear;
&:hover {
transform: scale(1.1);
}
&:focus-visible {
transform: scale(1.1);
}
&_reset {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: $border-divider;
color: $color-text-disabled;
padding: calc($padding-small / 2);
}
&_selected {
width: $color-item-size - 2 * $outline-space;
height: $color-item-size - 2 * $outline-space;
margin: $outline-space;
outline: $outline-width solid currentColor;
outline-offset: $outline-offset;
pointer-events: none;
}
}
}
&__preview {
width: $color-item-size;
height: auto;
}
}
}

View File

@@ -8,12 +8,10 @@ import Tooltip from "../../Main/Tooltip/Tooltip";
import LimitsConfigurator from "./LimitsConfigurator/LimitsConfigurator";
import { getAppModeEnable } from "../../../utils/app-mode";
import classNames from "classnames";
import TimezonesPicker from "./Timezones/TimezonesPicker";
import Timezones from "./Timezones/Timezones";
import ThemeControl from "../ThemeControl/ThemeControl";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useBoolean from "../../../hooks/useBoolean";
import BrowserTabController from "./BrowserTabController/BrowserTabController";
import LegendCollapseController from "./LegendCollapseController/LegendCollapseController";
const title = "Settings";
@@ -28,6 +26,7 @@ const GlobalSettings: FC = () => {
const serverSettingRef = useRef<ChildComponentHandle>(null);
const limitsSettingRef = useRef<ChildComponentHandle>(null);
const timezoneSettingRef = useRef<ChildComponentHandle>(null);
const {
value: open,
@@ -38,6 +37,7 @@ const GlobalSettings: FC = () => {
const handleApply = () => {
serverSettingRef.current && serverSettingRef.current.handleApply();
limitsSettingRef.current && limitsSettingRef.current.handleApply();
timezoneSettingRef.current && timezoneSettingRef.current.handleApply();
handleClose();
};
@@ -49,10 +49,6 @@ const GlobalSettings: FC = () => {
onClose={handleClose}
/>
},
{
show: true,
component: <TimezonesPicker/>
},
{
show: true,
component: <LimitsConfigurator
@@ -62,16 +58,12 @@ const GlobalSettings: FC = () => {
},
{
show: true,
component: <LegendCollapseController/>
component: <Timezones ref={timezoneSettingRef}/>
},
{
show: !appModeEnable,
component: <ThemeControl/>
},
{
show: true,
component: <BrowserTabController/>
},
}
].filter(control => control.show);
return <>

View File

@@ -1,30 +0,0 @@
import { FC, useEffect, useState } from "preact/compat";
import { getFromStorage, saveToStorage } from "../../../../utils/storage";
import Switch from "../../../Main/Switch/Switch";
import { LEGEND_COLLAPSE_SERIES_LIMIT } from "../../../../constants/graph";
import "./style.scss";
const LegendCollapseController: FC = () => {
const storageCollapse = getFromStorage("LEGEND_AUTO_COLLAPSE");
const [legendCollapse, setLegendCollapse] = useState(storageCollapse ? storageCollapse === "true" : true);
useEffect(() => {
saveToStorage("LEGEND_AUTO_COLLAPSE", `${legendCollapse}`);
}, [legendCollapse]);
return (
<div className="vm-legend-collapse-controller">
<Switch
fullWidth
color="neutral"
value={legendCollapse}
onChange={setLegendCollapse}
label={<span className="vm-server-configurator__title">Auto-collapse legend</span>}
/>
<span className="vm-legend-collapse-controller__description">
Collapses the legend when series count exceeds {LEGEND_COLLAPSE_SERIES_LIMIT} to reduce UI load.
</span>
</div>);
};
export default LegendCollapseController;

View File

@@ -1,19 +0,0 @@
@use "src/styles/variables" as *;
.vm-legend-collapse-controller {
background-color: $color-hover-black;
border-radius: $border-radius-medium;
padding: $padding-large;
border: $border-divider;
.vm-graph-settings-row__label {
margin: 0;
}
&__description {
padding-top: $padding-global;
font-size: $font-size-small;
line-height: 1.3;
text-wrap: pretty;
}
}

View File

@@ -1,21 +1,23 @@
import { forwardRef, useCallback, useImperativeHandle, useState } from "preact/compat";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "preact/compat";
import { DisplayType, ErrorTypes } from "../../../../types";
import TextField from "../../../Main/TextField/TextField";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import { InfoIcon, RestartIcon } from "../../../Main/Icons";
import Button from "../../../Main/Button/Button";
import { DEFAULT_MAX_SERIES } from "../../../../constants/graph";
import { DEFAULT_MAX_SERIES, LEGEND_COLLAPSE_SERIES_LIMIT } from "../../../../constants/graph";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import { ChildComponentHandle } from "../GlobalSettings";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../../state/customPanel/CustomPanelStateContext";
import Switch from "../../../Main/Switch/Switch";
import { getFromStorage, saveToStorage } from "../../../../utils/storage";
interface ServerConfiguratorProps {
onClose: () => void;
onClose: () => void
}
const fields: { label: string, type: DisplayType }[] = [
const fields: {label: string, type: DisplayType}[] = [
{ label: "Graph", type: DisplayType.chart },
{ label: "JSON", type: DisplayType.code },
{ label: "Table", type: DisplayType.table }
@@ -27,7 +29,8 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
const { seriesLimits } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
const storageCollapse = getFromStorage("LEGEND_AUTO_COLLAPSE");
const [legendCollapse, setLegendCollapse] = useState(storageCollapse ? storageCollapse === "true" : true);
const [limits, setLimits] = useState(seriesLimits);
const [error, setError] = useState({
@@ -40,7 +43,7 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
setLimits(DEFAULT_MAX_SERIES);
};
const createChangeHandler = (type: DisplayType) => (val: string) => {
const createChangeHandler = (type: DisplayType) => (val: string) => {
const value = val || "";
setError(prev => ({ ...prev, [type]: +value < 0 ? ErrorTypes.positiveNumber : "" }));
setLimits({
@@ -54,6 +57,10 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
onClose();
}, [limits]);
useEffect(() => {
saveToStorage("LEGEND_AUTO_COLLAPSE", `${legendCollapse}`);
}, [legendCollapse]);
useImperativeHandle(ref, () => ({ handleApply }), [handleApply]);
return (
@@ -99,6 +106,19 @@ const LimitsConfigurator = forwardRef<ChildComponentHandle, ServerConfiguratorPr
</div>
))}
</div>
<div className="vm-graph-settings-row">
<span className="vm-graph-settings-row__label">Auto-collapse legend</span>
<Switch
value={legendCollapse}
onChange={setLegendCollapse}
label={legendCollapse ? "Enabled" : "Disabled"}
fullWidth={isMobile}
/>
<span className="vm-legend-configs-item__info">
Collapses the legend when series count exceeds {LEGEND_COLLAPSE_SERIES_LIMIT} to reduce UI load.
</span>
</div>
</div>
);
});

View File

@@ -0,0 +1,183 @@
import { FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/compat";
import { getBrowserTimezone, getTimezoneList, getUTCByTimezone } from "../../../../utils/time";
import { ArrowDropDownIcon } from "../../../Main/Icons";
import classNames from "classnames";
import Popper from "../../../Main/Popper/Popper";
import Accordion from "../../../Main/Accordion/Accordion";
import TextField from "../../../Main/TextField/TextField";
import { Timezone } from "../../../../types";
import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
import WarningTimezone from "./WarningTimezone";
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
interface PinnedTimezone extends Timezone {
title: string;
isInvalid?: boolean;
}
const browserTimezone = getBrowserTimezone();
const Timezones: FC = forwardRef((props, ref) => {
const { isMobile } = useDeviceDetect();
const timezones = getTimezoneList();
const { timezone: stateTimezone, defaultTimezone } = useTimeState();
const timeDispatch = useTimeDispatch();
const [timezone, setTimezone] = useState(stateTimezone);
const [search, setSearch] = useState("");
const targetRef = useRef<HTMLDivElement>(null);
const {
value: openList,
toggle: toggleOpenList,
setFalse: handleCloseList,
} = useBoolean(false);
const pinnedTimezones = useMemo(() => [
{
title: `Default time (${defaultTimezone})`,
region: defaultTimezone,
utc: defaultTimezone ? getUTCByTimezone(defaultTimezone) : "UTC"
},
{
title: browserTimezone.title,
region: browserTimezone.region,
utc: getUTCByTimezone(browserTimezone.region),
isInvalid: !browserTimezone.isValid
},
{
title: "UTC (Coordinated Universal Time)",
region: "UTC",
utc: "UTC"
},
].filter(t => t.region) as PinnedTimezone[], [defaultTimezone]);
const searchTimezones = useMemo(() => {
if (!search) return timezones;
try {
return getTimezoneList(search);
} catch (e) {
return {};
}
}, [search, timezones]);
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
const activeTimezone = useMemo(() => ({
region: timezone,
utc: getUTCByTimezone(timezone)
}), [timezone]);
const handleChangeSearch = (val: string) => {
setSearch(val);
};
const handleSetTimezone = (val: Timezone) => {
setTimezone(val.region);
setSearch("");
handleCloseList();
};
const createHandlerSetTimezone = (val: Timezone) => () => {
handleSetTimezone(val);
};
useEffect(() => {
setTimezone(stateTimezone);
}, [stateTimezone]);
useImperativeHandle(ref, () => ({
handleApply: () => {
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
}
}), [timezone]);
return (
<div className="vm-timezones">
<div className="vm-server-configurator__title">
Time zone
</div>
<div
className="vm-timezones-item vm-timezones-item_selected"
onClick={toggleOpenList}
ref={targetRef}
>
<div className="vm-timezones-item__title">{activeTimezone.region}</div>
<div className="vm-timezones-item__utc">{activeTimezone.utc}</div>
<div
className={classNames({
"vm-timezones-item__icon": true,
"vm-timezones-item__icon_open": openList
})}
>
<ArrowDropDownIcon/>
</div>
</div>
<Popper
open={openList}
buttonRef={targetRef}
placement="bottom-left"
onClose={handleCloseList}
fullWidth
title={isMobile ? "Time zone" : undefined}
>
<div
className={classNames({
"vm-timezones-list": true,
"vm-timezones-list_mobile": isMobile,
})}
>
<div className="vm-timezones-list-header">
<div className="vm-timezones-list-header__search">
<TextField
autofocus
label="Search"
value={search}
onChange={handleChangeSearch}
/>
</div>
{pinnedTimezones.map((t, i) => t && (
<div
key={`${i}_${t.region}`}
className="vm-timezones-item vm-timezones-list-group-options__item"
onClick={createHandlerSetTimezone(t)}
>
<div className="vm-timezones-item__title">{t.title}{t.isInvalid && <WarningTimezone/>}</div>
<div className="vm-timezones-item__utc">{t.utc}</div>
</div>
))}
</div>
{timezonesGroups.map(t => (
<div
className="vm-timezones-list-group"
key={t}
>
<Accordion
defaultExpanded={true}
title={<div className="vm-timezones-list-group__title">{t}</div>}
>
<div className="vm-timezones-list-group-options">
{searchTimezones[t] && searchTimezones[t].map(item => (
<div
className="vm-timezones-item vm-timezones-list-group-options__item"
onClick={createHandlerSetTimezone(item)}
key={item.search}
>
<div className="vm-timezones-item__title">{item.region}</div>
<div className="vm-timezones-item__utc">{item.utc}</div>
</div>
))}
</div>
</Accordion>
</div>
))}
</div>
</Popper>
</div>
);
});
export default Timezones;

View File

@@ -1,129 +0,0 @@
import { FC, useMemo, useState } from "preact/compat";
import { getBrowserTimezone, getTimezoneList, getUTCByTimezone } from "../../../../utils/time";
import classNames from "classnames";
import Accordion from "../../../Main/Accordion/Accordion";
import TextField from "../../../Main/TextField/TextField";
import { Timezone } from "../../../../types";
import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import WarningTimezone from "./WarningTimezone";
import { useTimeState } from "../../../../state/time/TimeStateContext";
interface PinnedTimezone extends Timezone {
title: string;
isInvalid?: boolean;
}
type Props = {
onChange: (tz: Timezone) => void;
}
const browserTimezone = getBrowserTimezone();
const TimezonesList: FC<Props> = ({ onChange }) => {
const { isMobile } = useDeviceDetect();
const { defaultTimezone } = useTimeState();
const timezones = useMemo(() => getTimezoneList(), []);
const [search, setSearch] = useState("");
const pinnedTimezones = useMemo(() => [
{
title: `Default time (${defaultTimezone})`,
region: defaultTimezone,
utc: defaultTimezone ? getUTCByTimezone(defaultTimezone) : "UTC"
},
{
title: browserTimezone.title,
region: browserTimezone.region,
utc: getUTCByTimezone(browserTimezone.region),
isInvalid: !browserTimezone.isValid
},
{
title: "UTC (Coordinated Universal Time)",
region: "UTC",
utc: "UTC"
},
].filter(t => t.region) as PinnedTimezone[], [defaultTimezone]);
const searchTimezones = useMemo(() => {
if (!search) return timezones;
try {
return getTimezoneList(search);
} catch (e) {
return {};
}
}, [search, timezones]);
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
const handleChangeSearch = (val: string) => {
setSearch(val);
};
const handleSetTimezone = (tz: Timezone) => {
onChange(tz);
setSearch("");
};
const createHandlerSetTimezone = (val: Timezone) => () => {
handleSetTimezone(val);
};
return (
<div
className={classNames({
"vm-list": true,
"vm-timezones-list": true,
"vm-timezones-list_mobile": isMobile,
})}
>
<div className="vm-timezones-list-header">
<div className="vm-timezones-list-header__search">
<TextField
label="Search"
value={search}
onChange={handleChangeSearch}
/>
</div>
</div>
{pinnedTimezones.map((t, i) => t && (
<div
key={`${i}_${t.region}`}
className="vm-list-item vm-timezones-item vm-timezones-list-group-options__item"
onClick={createHandlerSetTimezone(t)}
>
<div className="vm-timezones-item__title">{t.title}{t.isInvalid && <WarningTimezone/>}</div>
<div className="vm-timezones-item__utc">{t.utc}</div>
</div>
))}
{timezonesGroups.map(t => (
<div
className="vm-timezones-list-group"
key={t}
>
<Accordion
defaultExpanded={true}
title={<div className="vm-timezones-list-group__title">{t}</div>}
>
<div className="vm-timezones-list-group-options">
{searchTimezones[t] && searchTimezones[t].map(item => (
<div
className="vm-list-item vm-timezones-item vm-timezones-list-group-options__item"
onClick={createHandlerSetTimezone(item)}
key={item.search}
>
<div className="vm-timezones-item__title">{item.region}</div>
<div className="vm-timezones-item__utc">{item.utc}</div>
</div>
))}
</div>
</Accordion>
</div>
))}
</div>
);
};
export default TimezonesList;

View File

@@ -1,71 +0,0 @@
import { FC, useMemo, useRef } from "preact/compat";
import { getUTCByTimezone } from "../../../../utils/time";
import { ArrowDropDownIcon } from "../../../Main/Icons";
import classNames from "classnames";
import { Timezone } from "../../../../types";
import "./style.scss";
import useBoolean from "../../../../hooks/useBoolean";
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
import TimezonesList from "./TimezonesList";
import Popper from "../../../Main/Popper/Popper";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
const TimezonesPicker: FC = () => {
const { isMobile } = useDeviceDetect();
const { timezone: stateTimezone } = useTimeState();
const timeDispatch = useTimeDispatch();
const triggerRef = useRef<HTMLDivElement>(null);
const {
value: isOpenList,
toggle: toggleOpenList,
setFalse: handleCloseList,
} = useBoolean(false);
const activeTimezone = useMemo(() => ({
region: stateTimezone,
utc: getUTCByTimezone(stateTimezone)
}), [stateTimezone]);
const handleSetTimezone = (tz: Timezone) => {
timeDispatch({ type: "SET_TIMEZONE", payload: tz.region });
handleCloseList();
};
return (
<div className="vm-timezones">
<div className="vm-server-configurator__title">
Time zone
</div>
<div
className="vm-timezones-item vm-timezones-item_selected"
onClick={toggleOpenList}
ref={triggerRef}
>
<div className="vm-timezones-item__title">{activeTimezone.region}</div>
<div className="vm-timezones-item__utc">{activeTimezone.utc}</div>
<div
className={classNames({
"vm-timezones-item__icon": true,
"vm-timezones-item__icon_open": isOpenList
})}
>
<ArrowDropDownIcon/>
</div>
</div>
<Popper
open={isOpenList}
buttonRef={triggerRef}
placement="bottom-left"
onClose={handleCloseList}
fullWidth
title={isMobile ? "Time zone" : undefined}
>
<TimezonesList onChange={handleSetTimezone}/>
</Popper>
</div>
);
};
export default TimezonesPicker;

View File

@@ -16,7 +16,6 @@
}
&__title {
flex-grow: 1;
display: flex;
align-items: center;
gap: $padding-small;
@@ -35,7 +34,6 @@
background-color: $color-hover-black;
padding: calc($padding-small/2);
border-radius: $border-radius-small;
font-size: $font-size-small;
}
&__icon {
@@ -56,11 +54,9 @@
}
&-list {
padding-top: 0;
max-height: 300px;
background-color: $color-background-block;
border-radius: $border-radius-medium;
font-size: $font-size-small;
overflow: auto;
&_mobile {
@@ -76,9 +72,10 @@
top: 0;
background-color: $color-background-block;
z-index: 2;
border-bottom: $border-divider;
&__search {
padding: $padding-small $padding-small calc($padding-small / 2);
padding: $padding-small;
}
}
@@ -94,7 +91,6 @@
font-weight: bold;
color: $color-text-secondary;
padding: $padding-small $padding-global;
font-size: $font-size-small;
}
&-options {
@@ -102,7 +98,7 @@
align-items: flex-start;
&__item {
padding: calc($padding-small / 2) $padding-global;
padding: $padding-small $padding-global;
transition: background-color 200ms ease;
&:hover {

View File

@@ -4,9 +4,9 @@
display: flex;
flex-direction: column;
align-items: center;
gap: calc($padding-global * 2);
gap: $padding-large;
width: 600px;
padding-inline: $padding-large;
padding-bottom: $padding-medium;
&_mobile {
grid-auto-rows: min-content;
@@ -62,7 +62,6 @@
justify-content: flex-end;
gap: $padding-small;
width: 100%;
padding-block: $padding-global;
}
&_mobile &-footer {

View File

@@ -22,10 +22,12 @@ const StepConfigurator: FC = () => {
const { isMobile } = useDeviceDetect();
const { customStep: value, isHistogram } = useGraphState();
const { period: { end, start } } = useTimeState();
const { period: { step, end, start } } = useTimeState();
const graphDispatch = useGraphDispatch();
const { displayType } = useCustomPanelState();
const prevDuration = usePrevious(end - start);
const defaultStep = useMemo(() => {
return getStepFromDuration(end - start, isHistogram, displayType);
}, [end, start, isHistogram, displayType]);
@@ -104,14 +106,16 @@ const StepConfigurator: FC = () => {
}, [defaultStep]);
useEffect(() => {
if (!prevDefaultStep) return;
if (value !== prevDefaultStep) return;
if (value === defaultStep) return;
const dur = end - start;
if (dur === prevDuration || !prevDuration || value !== prevDefaultStep) return;
if (defaultStep) {
handleApply(defaultStep);
}
}, [prevDuration, defaultStep]);
graphDispatch({ type: "SET_CUSTOM_STEP", payload: defaultStep });
setCustomStep(defaultStep);
setError("");
}, [defaultStep, prevDefaultStep, value, graphDispatch]);
useEffect(() => {
if (step === value || step === defaultStep) handleApply(defaultStep);
}, [isHistogram, displayType]);
return (
<div

View File

@@ -5,20 +5,8 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
import { FC } from "preact/compat";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { DarkIcon, LightIcon, SystemIcon } from "../../Main/Icons";
const themeIcons = {
[Theme.system]: <SystemIcon/>,
[Theme.light]: <LightIcon/>,
[Theme.dark]: <DarkIcon/>,
};
const options = Object.values(Theme).map(value => ({
title: value,
value,
icon: themeIcons[value],
}));
const options = Object.values(Theme).map(value => ({ title: value, value }));
const ThemeControl: FC = () => {
const { isMobile } = useDeviceDetect();
const dispatch = useAppDispatch();
@@ -37,14 +25,13 @@ const ThemeControl: FC = () => {
})}
>
<div className="vm-server-configurator__title">
Theme
Theme preferences
</div>
<div
className="vm-theme-control__toggle"
key={`${isMobile}`}
>
<Toggle
size="large"
options={options}
value={theme}
onChange={handleClickItem}

View File

@@ -4,7 +4,7 @@
&__toggle {
display: inline-flex;
width: 100%;
min-width: 300px;
text-transform: capitalize;
}

View File

@@ -1,4 +1,4 @@
import { FC, useEffect, useRef } from "preact/compat";
import { FC, useEffect, useRef, useState } from "preact/compat";
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
import { getAppModeEnable } from "../../../../utils/app-mode";
import Button from "../../../Main/Button/Button";
@@ -9,38 +9,27 @@ import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import useBoolean from "../../../../hooks/useBoolean";
import { getMillisecondsFromDuration } from "../../../../utils/time";
import { useSearchParams } from "react-router";
const delayOptions = [
"Off",
"1s",
"2s",
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h"
interface AutoRefreshOption {
seconds: number
title: string
}
const delayOptions: AutoRefreshOption[] = [
{ seconds: 0, title: "Off" },
{ seconds: 1, title: "1s" },
{ seconds: 2, title: "2s" },
{ seconds: 5, title: "5s" },
{ seconds: 10, title: "10s" },
{ seconds: 30, title: "30s" },
{ seconds: 60, title: "1m" },
{ seconds: 300, title: "5m" },
{ seconds: 900, title: "15m" },
{ seconds: 1800, title: "30m" },
{ seconds: 3600, title: "1h" },
{ seconds: 7200, title: "2h" }
];
const DEFAULT_OPTION = delayOptions[0];
const MIN_REFRESH_MS = 1000;
const MAX_REFRESH_MS = getMillisecondsFromDuration(delayOptions[delayOptions.length - 1]);
const REFRESH_URL_PARAM = "refresh";
const durationToMs = (dur: string | null) => {
return dur ? getMillisecondsFromDuration(dur) : 0;
};
const isValidDelay = (ms: number) => {
return ms >= MIN_REFRESH_MS && ms <= MAX_REFRESH_MS;
};
interface ExecutionControlsProps {
tooltip: string;
useAutorefresh?: boolean;
@@ -49,14 +38,12 @@ interface ExecutionControlsProps {
export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAutorefresh, closeModal }) => {
const { isMobile } = useDeviceDetect();
const [searchParams, setSearchParams] = useSearchParams();
const dispatch = useTimeDispatch();
const appModeEnable = getAppModeEnable();
const [autoRefresh, setAutoRefresh] = useState(false);
const rawDelay = searchParams.get(REFRESH_URL_PARAM);
const msDelay = durationToMs(rawDelay);
const selectedDelay = isValidDelay(msDelay) ? rawDelay : DEFAULT_OPTION;
const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
const {
value: openOptions,
@@ -65,20 +52,11 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
} = useBoolean(false);
const optionsButtonRef = useRef<HTMLDivElement>(null);
const handleChange = (dur: string) => () => {
setSearchParams(prev => {
const nextParams = new URLSearchParams(prev);
const ms = durationToMs(dur);
if (ms) {
nextParams.set(REFRESH_URL_PARAM, `${dur}`);
} else {
nextParams.delete(REFRESH_URL_PARAM);
}
return nextParams;
});
const handleChange = (d: AutoRefreshOption) => {
if ((autoRefresh && !d.seconds) || (!autoRefresh && d.seconds)) {
setAutoRefresh(prev => !prev);
}
setSelectedDelay(d);
handleCloseOptions();
};
@@ -90,19 +68,23 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
};
useEffect(() => {
const ms = durationToMs(selectedDelay);
const delay = selectedDelay.seconds;
let timer: number;
if (useAutorefresh && isValidDelay(ms)) {
if (autoRefresh) {
timer = setInterval(() => {
dispatch({ type: "RUN_QUERY" });
}, ms) as unknown as number;
}, delay * 1000) as unknown as number;
} else {
setSelectedDelay(delayOptions[0]);
}
return () => {
clearInterval(timer);
timer && clearInterval(timer);
};
}, [selectedDelay, useAutorefresh]);
}, [selectedDelay, autoRefresh]);
const createHandlerChange = (d: AutoRefreshOption) => () => {
handleChange(d);
};
return (
<>
@@ -124,7 +106,7 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
<span className="vm-mobile-option__icon"><RestartIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Auto-refresh</span>
<span className="vm-mobile-option-text__value">{selectedDelay}</span>
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
@@ -157,7 +139,7 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
)}
onClick={toggleOpenOptions}
>
{selectedDelay}
{selectedDelay.title}
</Button>
</div>
</Tooltip>
@@ -205,12 +187,12 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
className={classNames({
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": d === selectedDelay
"vm-list-item_active": d.seconds === selectedDelay.seconds
})}
key={d}
onClick={handleChange(d)}
key={d.seconds}
onClick={createHandlerChange(d)}
>
{d}
{d.title}
</div>
))}
</div>

View File

@@ -633,60 +633,3 @@ export const DebugIcon = () => (
/>
</svg>
);
export const SystemIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M18 4C18.7957 4 19.5595 4.3163 20.1221 4.87891C20.6845 5.44148 21 6.20452 21 7V15.5264L21.0069 15.6426C21.0203 15.7579 21.0542 15.8702 21.1065 15.9746L22.1729 18.0996C22.3271 18.4056 22.4009 18.7466 22.3858 19.0889C22.3705 19.431 22.2675 19.7637 22.0869 20.0547C21.9063 20.3457 21.6534 20.5855 21.3535 20.751C21.0555 20.9154 20.7202 21.0003 20.3799 20.999L3.62013 21C3.28002 21.0012 2.94535 20.9153 2.64748 20.751C2.34748 20.5855 2.09477 20.3458 1.91408 20.0547C1.73343 19.7636 1.63047 19.4311 1.61525 19.0889C1.60006 18.7466 1.67297 18.4056 1.82716 18.0996L2.89455 15.9746L2.94045 15.8682C2.9801 15.7589 3.00007 15.6432 3.00002 15.5264V7C3.00002 6.20442 3.3164 5.4415 3.87892 4.87891C4.44146 4.31636 5.20447 4.00007 6.00002 4H18ZM4.62404 16.9873L3.61427 18.999L3.6133 19H20.3877L20.3867 18.999L19.376 16.9873H4.62404ZM6.00002 6C5.7349 6.00007 5.48045 6.1055 5.29298 6.29297C5.10554 6.48049 5.00002 6.73485 5.00002 7V14.9873H19V7C19 6.73478 18.8946 6.48051 18.707 6.29297C18.5195 6.10552 18.2652 6 18 6H6.00002Z"
/>
</svg>
);
export const LightIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 19C12.5523 19 13 19.4477 13 20V22C13 22.5523 12.5523 23 12 23C11.4477 23 11 22.5523 11 22V20C11 19.4477 11.4477 19 12 19Z"
/>
<path
d="M5.63281 16.9531C6.02334 16.5627 6.65638 16.5626 7.04688 16.9531C7.43717 17.3436 7.43725 17.9767 7.04688 18.3672L5.63672 19.7773C5.24625 20.1676 4.61313 20.1676 4.22266 19.7773C3.8322 19.3869 3.83233 18.7538 4.22266 18.3633L5.63281 16.9531Z"
/>
<path
d="M16.9531 16.9531C17.3436 16.5626 17.9767 16.5626 18.3672 16.9531L19.7773 18.3633C20.1676 18.7538 20.1678 19.3869 19.7773 19.7773C19.3869 20.1677 18.7538 20.1675 18.3633 19.7773L16.9531 18.3672C16.5626 17.9767 16.5627 17.3437 16.9531 16.9531Z"
/>
<path
d="M12 7C14.7614 7 17 9.23858 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7ZM12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9Z"
/>
<path
d="M4 11C4.55228 11 5 11.4477 5 12C5 12.5523 4.55228 13 4 13H2C1.44772 13 1 12.5523 1 12C1 11.4477 1.44772 11 2 11H4Z"
/>
<path
d="M22 11C22.5523 11 23 11.4477 23 12C23 12.5523 22.5523 13 22 13H20C19.4477 13 19 12.5523 19 12C19 11.4477 19.4477 11 20 11H22Z"
/>
<path
d="M4.22266 4.22266C4.61315 3.83229 5.24623 3.83229 5.63672 4.22266L7.04688 5.63281C7.4372 6.02331 7.43723 6.65639 7.04688 7.04688C6.6564 7.43735 6.02335 7.43724 5.63281 7.04688L4.22266 5.63672C3.83225 5.24618 3.83217 4.61314 4.22266 4.22266Z"
/>
<path
d="M18.3633 4.22266C18.7538 3.83237 19.3869 3.83232 19.7773 4.22266C20.1677 4.61312 20.1676 5.2462 19.7773 5.63672L18.3672 7.04688C17.9767 7.4373 17.3436 7.4373 16.9531 7.04688C16.5627 6.65637 16.5627 6.0233 16.9531 5.63281L18.3633 4.22266Z"
/>
<path
d="M12 1C12.5523 1 13 1.44772 13 2V4C13 4.55228 12.5523 5 12 5C11.4477 5 11 4.55228 11 4V2C11 1.44772 11.4477 1 12 1Z"
/>
</svg>
);
export const DarkIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M11.5809 2.01318C12.1851 2.02907 12.6373 2.40742 12.8475 2.84717C13.0627 3.29758 13.0619 3.86893 12.7615 4.34814L12.7606 4.34717C12.1616 5.30581 11.9061 6.43987 12.034 7.56299C12.162 8.68628 12.6672 9.73328 13.4666 10.5327C14.2661 11.332 15.3131 11.8364 16.4363 11.9644C17.5596 12.0922 18.6934 11.836 19.6522 11.2368L19.8348 11.1382C20.2701 10.9405 20.7563 10.9617 21.1512 11.1499C21.6217 11.3742 22.0194 11.8752 21.9832 12.5405C21.8789 14.4693 21.2181 16.3268 20.0809 17.8882C18.9435 19.4496 17.378 20.6484 15.574 21.3394C13.7701 22.0302 11.8042 22.184 9.91485 21.7817C8.02549 21.3794 6.29356 20.4376 4.92754 19.0718C3.56149 17.7059 2.62012 15.9739 2.21758 14.0845C1.81507 12.195 1.96826 10.2294 2.65899 8.42529C3.34975 6.62115 4.5487 5.05596 6.11016 3.91846C7.6716 2.781 9.52882 2.11947 11.4578 2.01514L11.5809 2.01318ZM10.6209 4.12061C9.42038 4.33038 8.27873 4.81214 7.28692 5.53467C6.03798 6.44459 5.07973 7.69705 4.52715 9.14014C3.97456 10.5834 3.85165 12.156 4.17364 13.6675C4.49565 15.179 5.24872 16.5649 6.34161 17.6577C7.43448 18.7505 8.82026 19.5038 10.3318 19.8257C11.8434 20.1475 13.416 20.0239 14.8592 19.4712C16.3024 18.9184 17.5548 17.9597 18.4647 16.7104C19.1869 15.7188 19.6671 14.5776 19.8768 13.3774C18.7333 13.8927 17.4674 14.0949 16.2098 13.9517C14.637 13.7725 13.1709 13.0661 12.0516 11.9468C10.9324 10.8275 10.2258 9.36126 10.0467 7.78857C9.90352 6.53075 10.1054 5.26417 10.6209 4.12061Z"
/>
</svg>
);

View File

@@ -1,10 +1,11 @@
import { FC, ReactNode } from "preact/compat";
import { ReactNode } from "react";
import classNames from "classnames";
import "./style.scss";
import { FC } from "preact/compat";
interface SwitchProps {
value: boolean
color?: "primary" | "secondary" | "error" | "neutral"
color?: "primary" | "secondary" | "error"
disabled?: boolean
label?: string | ReactNode
fullWidth?: boolean

View File

@@ -29,10 +29,6 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
background-color: $color-secondary;
}
&_neutral_active &-track {
background-color: $color-text;
}
&_primary_active &-track {
background-color: $color-primary;
}

View File

@@ -1,21 +1,22 @@
import { FC, useEffect, useRef, useState, ReactNode } from "preact/compat";
import { FC, useEffect, useRef, useState } from "preact/compat";
import classNames from "classnames";
import { ReactNode } from "react";
import "./style.scss";
interface ToggleProps {
options: { value: string, title?: string, icon?: ReactNode }[];
value: string;
onChange: (val: string) => void;
label?: string;
size?: "medium" | "large";
options: {value: string, title?: string, icon?: ReactNode}[]
value: string
onChange: (val: string) => void
label?: string
}
const Toggle: FC<ToggleProps> = ({ options, value, label, size = "medium", onChange }) => {
const Toggle: FC<ToggleProps> = ({ options, value, label, onChange }) => {
const activeRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({
width: "0px",
left: "0px",
borderRadius: "0px"
});
const createHandlerChange = (value: string) => () => {
@@ -27,25 +28,35 @@ const Toggle: FC<ToggleProps> = ({ options, value, label, size = "medium", onCha
setPosition({
width: "0px",
left: "0px",
borderRadius: "0px"
});
return;
}
const index = options.findIndex(o => o.value === value);
const { width: widthRect } = activeRef.current.getBoundingClientRect();
const width = widthRect;
const left = index * width;
let width = widthRect;
let left = index * width;
let borderRadius = "0";
if (index === 0) borderRadius = "16px 0 0 16px";
setPosition({ width: `${width}px`, left: `${left}px` });
if (index === options.length - 1) {
borderRadius = "10px";
left -= 1;
borderRadius = "0 16px 16px 0";
}
if (index !== 0 && (index !== options.length - 1)) {
width += 1;
left -= 1;
}
setPosition({ width: `${width}px`, left: `${left}px`, borderRadius });
}, [activeRef, value, options]);
return (
<div
className={classNames({
"vm-toggles": true,
[`vm-toggles_${size}`]: size,
})}
>
<div className="vm-toggles">
{label && (
<label className="vm-toggles__label">
{label}
@@ -55,14 +66,15 @@ const Toggle: FC<ToggleProps> = ({ options, value, label, size = "medium", onCha
className="vm-toggles-group"
style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}
>
<div
{position.borderRadius && <div
className="vm-toggles-group__highlight"
style={position}
/>
{options.map((option) => (
/>}
{options.map((option, i) => (
<div
className={classNames({
"vm-toggles-group-item": true,
"vm-toggles-group-item_first": i === 0,
"vm-toggles-group-item_active": option.value === value,
"vm-toggles-group-item_icon": option.icon && option.title
})}

View File

@@ -20,8 +20,6 @@
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: $border-radius-small;
background: $color-hover-black;
&-item {
position: relative;
@@ -29,68 +27,55 @@
align-items: center;
justify-content: center;
padding: $padding-small;
border-right: $border-divider;
border-top: $border-divider;
border-bottom: $border-divider;
font-size: $font-size-small;
color: $color-text-secondary;
font-weight: 500;
font-weight: bold;
cursor: pointer;
text-align: center;
transition: opacity 150ms ease-in, color 150ms ease-in;
transition: color 150ms ease-in;
z-index: 2;
user-select: none;
&_icon {
grid-template-columns: 14px auto;
gap: calc($padding-small / 2);
&_first {
border-radius: 16px 0 0 16px;
border-left: $border-divider
}
&:hover:not(&_active) {
opacity: 0.8;
&:last-child {
border-radius: 0 16px 16px 0;
border-left: none;
}
&_icon {
grid-template-columns: 14px auto;
gap: 4px;
}
&:hover {
color: $color-primary;
}
&_active {
color: $color-text;
font-weight: 600;
color: $color-primary;
border-color: transparent;
&:hover {
background-color: transparent;
}
}
}
&__highlight {
position: absolute;
top: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
height: 100%;
background-color: rgba($color-primary, 0.08);
border: 1px solid $color-primary;
transition: left 200ms cubic-bezier(0.280, 0.840, 0.420, 1), border-radius 200ms linear;
z-index: 1;
&:after {
content: '';
height: 100%;
width: 100%;
background-color: $color-background-block;
border-radius: $border-radius-small;
box-shadow: $box-shadow;
}
}
}
&_large &-group {
border-radius: $border-radius-medium;
&-item {
padding: $padding-global $padding-small;
&_icon {
grid-template-columns: 16px auto;
gap: $padding-small;
}
}
&__highlight {
padding: 4px;
border-radius: $border-radius-medium;
}
}
}

View File

@@ -1,14 +0,0 @@
export const faviconColors = [
"#A1A1AA",
"#71717A",
"#020202",
"#E94600",
"#FF7A00",
"#F2B705",
"#84CC16",
"#16B86A",
"#00AFAF",
"#2979FF",
"#8B5CF6",
"#E83E9A",
] as const;

View File

@@ -14,9 +14,6 @@ import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
import WebStorageCheck from "../../components/WebStorageCheck/WebStorageCheck";
import { migrateStorageToPrefixedKeys } from "../../utils/storage";
import {
useBrowserTabSync
} from "../../components/Configurators/GlobalSettings/BrowserTabController/hooks/useBrowserTabSync";
const MainLayout: FC = () => {
const appModeEnable = getAppModeEnable();
@@ -24,7 +21,6 @@ const MainLayout: FC = () => {
const { pathname } = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
useBrowserTabSync();
useFetchDashboards();
useFetchDefaultTimezone();
useFetchAppConfig();

View File

@@ -32,7 +32,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
const match = searchParams.get("match");
const focusLabel = searchParams.get("focusLabel");
const isMetric = /__name__/.test(match || "");
const showMetricNameStats = !(match || focusLabel);
const progress = totalSeries / totalSeriesAll * 100;
const diff = totalSeries - totalSeriesPrev;
const dynamic = Math.abs(diff) / totalSeriesPrev * 100;
@@ -56,7 +56,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
},
].filter(t => t.display);
if (!totals.length && !showMetricNameStats) {
if (!totals.length) {
return null;
}
@@ -75,7 +75,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
<h4 className="vm-cardinality-totals-card__title">
{info && (
<Tooltip title={<p className="vm-cardinality-totals-card__tooltip">{info}</p>}>
<div className="vm-cardinality-totals-card__info-icon"><InfoOutlinedIcon /></div>
<div className="vm-cardinality-totals-card__info-icon"><InfoOutlinedIcon/></div>
</Tooltip>
)}
{title}
@@ -99,10 +99,7 @@ const CardinalityTotals: FC<CardinalityTotalsProps> = ({
)}
</div>
))}
{
showMetricNameStats &&
<CardinalityMetricNameStats metricNameStats={metricNameStats} />
}
<CardinalityMetricNameStats metricNameStats={metricNameStats}/>
</div>
);
};

View File

@@ -72,7 +72,7 @@ const Relabel: FC = () => {
<div className="vm-relabeling-header-configs">
<TextField
type="textarea"
label="Config"
label="Relabel configs"
value={config}
autofocus
onChange={handleChangeConfig}
@@ -82,9 +82,8 @@ const Relabel: FC = () => {
<div className="vm-relabeling-header__labels">
<TextField
type="textarea"
label="A time series"
label="Labels"
value={labels}
placeholder="up{job=&quot;job_name&quot;,instance=&quot;host:port&quot;}"
onChange={handleChangeLabels}
onEnter={handleRunQuery}
/>
@@ -98,22 +97,22 @@ const Relabel: FC = () => {
<Button
variant="text"
color="gray"
startIcon={<WikiIcon/>}
startIcon={<InfoIcon/>}
>
Relabeling cookbook
</Button>
</a>
<a
target="_blank"
href="https://docs.victoriametrics.com/victoriametrics/relabeling/#relabeling-stages"
href="https://docs.victoriametrics.com/victoriametrics/relabeling/"
rel="help noreferrer"
>
<Button
variant="text"
color="gray"
startIcon={<InfoIcon/>}
startIcon={<WikiIcon/>}
>
Relabeling Stages
Documentation
</Button>
</a>
<Button

View File

@@ -1,29 +0,0 @@
import faviconRaw from "../../assets/favicon.svg?raw";
export const createFaviconUrl = (color = "#020202"): string => {
const svgDocument = new DOMParser().parseFromString(faviconRaw, "image/svg+xml");
const svg = svgDocument.documentElement;
if (svg.localName !== "svg") {
throw new Error("Invalid favicon SVG");
}
svg.setAttribute("fill", color);
const serializedSvg = new XMLSerializer().serializeToString(svg);
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(serializedSvg)}`;
};
export const updateFaviconColor = (color = "#020202"): void => {
const favicon = document.querySelector<HTMLLinkElement>("#favicon");
if (favicon) {
favicon.href = createFaviconUrl(color);
}
const maskIcon = document.querySelector<HTMLLinkElement>("#mask-icon");
if (maskIcon) {
maskIcon.setAttribute("color", color);
}
};
export const getFaviconStorageKey = () => window.location.pathname.replace(/\/+$/, "") || "/";

View File

@@ -17,11 +17,7 @@ export const ALL_STORAGE_KEYS = [
"POINTS_SHOW_ALL",
] as const;
export type FaviconStorageKey = `FAVICON_COLOR:${string}`;
export type StorageKeys =
| (typeof ALL_STORAGE_KEYS)[number]
| FaviconStorageKey;
export type StorageKeys = (typeof ALL_STORAGE_KEYS)[number];
type PrefixedStorageKeys = `${typeof STORAGE_PREFIX}${StorageKeys}`;
@@ -62,10 +58,7 @@ export const getFromStorage = (key: StorageKeys, withPrefix = true): undefined |
export const removeFromStorage = (keys: StorageKeys[], withPrefix = true): void => {
const storageKeys = withPrefix ? keys.map(toPrefixedKey) : keys;
storageKeys.forEach(k => {
window.localStorage.removeItem(k);
window.dispatchEvent(new StorageEvent("storage", { key: k }));
});
storageKeys.forEach(k => window.localStorage.removeItem(k));
};
/**

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import dayjs from "dayjs";
import { getMillisecondsFromDuration, getNanoTimestamp, parseSupportedDuration } from "./time";
import { getNanoTimestamp, parseSupportedDuration } from "./time";
describe("Time utils", () => {
describe("getNanoTimestamp", () => {
@@ -82,19 +82,4 @@ describe("Time utils", () => {
expect(parseSupportedDuration(" ")).toBeUndefined();
});
});
describe("getMillisecondsFromDuration", () => {
it("should convert valid durations to milliseconds", () => {
expect(getMillisecondsFromDuration("1s")).toBe(1000);
expect(getMillisecondsFromDuration("1.5s")).toBe(1500);
expect(getMillisecondsFromDuration("1m30s")).toBe(90000);
expect(getMillisecondsFromDuration("1h 30m")).toBe(5400000);
expect(getMillisecondsFromDuration("500ms")).toBe(500);
});
it("should return zero when duration cannot be parsed", () => {
expect(getMillisecondsFromDuration("garbage")).toBe(0);
expect(getMillisecondsFromDuration("")).toBe(0);
});
});
});

View File

@@ -100,10 +100,6 @@ export const getSecondsFromDuration = (dur: string) => {
return dayjs.duration(durObject).asSeconds();
};
export const getMillisecondsFromDuration = (dur: string): number => {
return getSecondsFromDuration(dur) * 1000;
};
const instantQueryViews = [DisplayType.table, DisplayType.code];
export const getStepFromDuration = (dur: number, histogram?: boolean, displayType?: DisplayType): string => {
if (displayType && instantQueryViews.includes(displayType)) return roundStep(dur);
@@ -205,21 +201,19 @@ export const getUTCByTimezone = (timezone: string) => {
};
export const getTimezoneList = (search = "") => {
const normalizedSearch = search.toLowerCase();
const regexp = new RegExp(search, "i");
return supportedTimezones.reduce((acc: { [key: string]: Timezone[] }, region) => {
return supportedTimezones.reduce((acc: {[key: string]: Timezone[]}, region) => {
const zone = (region.match(/^(.*?)\//) || [])[1] || "unknown";
const utc = getUTCByTimezone(region);
const utcForSearch = utc.replace(/^UTC/, "");
const utcForSearch = utc.replace(/UTC|0/, "");
const regionForSearch = region.replace(/[/_]/g, " ");
const item = {
region,
utc,
search: `${region} ${utc} ${regionForSearch} ${utcForSearch}`
};
const includeZone = !normalizedSearch || item.search.toLowerCase().includes(normalizedSearch);
const includeZone = !search || (search && regexp.test(item.search));
if (includeZone && acc[zone]) {
acc[zone].push(item);
@@ -282,3 +276,4 @@ export const getNanoTimestamp = (dateStr: string): bigint => {
// Return the full timestamp in nanoseconds as a BigInt
return BigInt(baseMs) * 1000000n + BigInt(extraNano);
};

View File

@@ -50,13 +50,6 @@ export default defineConfig(() => {
return "vendor";
}
},
assetFileNames: (assetInfo) => {
if (assetInfo.names.includes("favicon.svg")) {
return "assets/favicon.svg";
}
return "assets/[name]-[hash][extname]";
},
},
},
},

View File

@@ -135,7 +135,7 @@ func tenantViaURL(addr, prefix, tenant, suffix string) string {
}
// tenantViaHeaders returns path in cluster's URL format where tenant is omitted in URL
// Only supported if -enableMultitenancyViaHeaders is enabled
// Only supported if -enableMultitenancyViaHeaders is specified
func tenantViaHeaders(addr, prefix, suffix string) string {
return fmt.Sprintf("http://%s/%s/%s", addr, prefix, suffix)
}

View File

@@ -25,10 +25,12 @@ func TestClusterMultiTenantSelectViaHeaders(t *testing.T) {
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage.VminsertAddr(),
"-enableMultitenancyViaHeaders",
})
vmselect := tc.MustStartVmselect("vmselect", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
"-enableMultitenancyViaHeaders",
})
multitenant := make(http.Header)

View File

@@ -26,20 +26,33 @@ func TestClusterSearchWithDisabledPerDayIndex(t *testing.T) {
defer tc.Stop()
testSearchWithDisabledPerDayIndex(tc, func(name string, disablePerDayIndex bool) apptest.PrometheusWriteQuerier {
vmstorage := tc.MustStartVmstorage("vmstorage-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
// Using static ports for vmstorage because random ports may cause
// changes in how data is sharded.
vmstorage1 := tc.MustStartVmstorage("vmstorage1-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage1",
"-retentionPeriod=100y",
"-httpListenAddr=127.0.0.1:61001",
"-vminsertAddr=127.0.0.1:61002",
"-vmselectAddr=127.0.0.1:61003",
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
})
vmstorage2 := tc.MustStartVmstorage("vmstorage2-"+name, []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage2",
"-retentionPeriod=100y",
"-httpListenAddr=127.0.0.1:62001",
"-vminsertAddr=127.0.0.1:62002",
"-vmselectAddr=127.0.0.1:62003",
fmt.Sprintf("-disablePerDayIndex=%t", disablePerDayIndex),
})
vminsert := tc.MustStartVminsert("vminsert-"+name, []string{
"-storageNode=" + vmstorage.VminsertAddr(),
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
})
vmselect := tc.MustStartVmselect("vmselect"+name, []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
"-search.maxStalenessInterval=1m",
})
return &apptest.Vmcluster{
Vmstorages: []*apptest.Vmstorage{vmstorage},
Vmstorages: []*apptest.Vmstorage{vmstorage1, vmstorage2},
Vminsert: vminsert,
Vmselect: vmselect,
}

View File

@@ -59,7 +59,7 @@ func (ms *PrometheusMockStorage) Read(_ context.Context, query *prompb.Query, so
}
if !notMatch {
q.Timeseries = append(q.Timeseries, &prompb.TimeSeries{Labels: s.Labels, Samples: s.Samples, Histograms: s.Histograms})
q.Timeseries = append(q.Timeseries, &prompb.TimeSeries{Labels: s.Labels, Samples: s.Samples})
}
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage/remote"
@@ -87,17 +86,10 @@ func (rrs *RemoteReadServer) getReadHandler(t *testing.T) http.Handler {
samples = append(samples, sample)
}
}
var histograms []prompb.Histogram
for _, h := range s.Histograms {
if h.Timestamp >= startTs && h.Timestamp < endTs {
histograms = append(histograms, h)
}
}
var series prompb.TimeSeries
if len(samples) > 0 || len(histograms) > 0 {
if len(samples) > 0 {
series.Labels = s.Labels
series.Samples = samples
series.Histograms = histograms
}
ts[i] = &series
}
@@ -325,37 +317,6 @@ func generateRemoteReadSamples(idx int, startTime, endTime, numOfSamples int64)
return samples
}
// GenerateRemoteReadHistogramSeries generates a remote read series
// with native histogram samples within the given time range.
func GenerateRemoteReadHistogramSeries(start, end, numOfSamples int64) []*prompb.TimeSeries {
timeSeries := &prompb.TimeSeries{
Labels: []prompb.Label{
{Name: labels.MetricName, Value: "vm_histogram_metric"},
{Name: "job", Value: "0"},
},
}
delta := (end - start) / numOfSamples
mul := int64(0)
for t := start; t != end; t += delta {
mul++
h := &histogram.Histogram{
Schema: 0,
Count: uint64(10 * mul),
Sum: 25.5 * float64(mul),
ZeroThreshold: 0.001,
ZeroCount: uint64(2 * mul),
PositiveSpans: []histogram.Span{{Offset: 0, Length: 2}},
PositiveBuckets: []int64{mul, 2 * mul},
NegativeSpans: []histogram.Span{{Offset: 0, Length: 1}},
NegativeBuckets: []int64{4 * mul},
}
timeSeries.Histograms = append(timeSeries.Histograms, prompb.FromIntHistogram(t*1000, h))
}
return []*prompb.TimeSeries{timeSeries}
}
func labelsToLabelsProto(ls labels.Labels) []prompb.Label {
result := make([]prompb.Label, 0, ls.Len())
ls.Range(func(l labels.Label) {

View File

@@ -594,6 +594,7 @@ func TestSingleVMAgentMultitenancy(t *testing.T) {
fmt.Sprintf(`-remoteWrite.url=%s/api/v1/write`, remoteWriteSrv.URL),
"-remoteWrite.tmpDataPath=" + tc.Dir() + "/vmagent-multitenancy",
"-enableMultitenantHandlers",
"-enableMultitenancyViaHeaders",
})
vmagent.APIV1ImportPrometheus(t, []string{

View File

@@ -75,118 +75,6 @@ func TestClusterVmctlRemoteReadProtocol(t *testing.T) {
testRemoteReadProtocol(tc, clusterDst, newRemoteReadServer, vmctlFlags)
}
func TestSingleVmctlRemoteReadNativeHistograms(t *testing.T) {
fs.MustRemoveDir(t.Name())
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmsingleDst := tc.MustStartDefaultVmsingle()
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
vmctlFlags := []string{
`remote-read`,
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
`--remote-read-step-interval=minute`,
`--vm-addr=` + vmAddr,
`--disable-progress-bar=true`,
}
testRemoteReadNativeHistograms(tc, vmsingleDst, NewRemoteReadServer, vmctlFlags)
}
func TestSingleVmctlRemoteReadStreamNativeHistograms(t *testing.T) {
fs.MustRemoveDir(t.Name())
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmsingleDst := tc.MustStartDefaultVmsingle()
vmAddr := fmt.Sprintf("http://%s/", vmsingleDst.HTTPAddr())
vmctlFlags := []string{
`remote-read`,
`--remote-read-filter-time-start=2025-06-11T15:31:10Z`,
`--remote-read-filter-time-end=2025-06-11T15:31:20Z`,
`--remote-read-step-interval=minute`,
`--vm-addr=` + vmAddr,
`--remote-read-use-stream=true`,
`--disable-progress-bar=true`,
}
testRemoteReadNativeHistograms(tc, vmsingleDst, NewRemoteReadStreamServer, vmctlFlags)
}
// testRemoteReadNativeHistograms verifies that native histograms are migrated
// as _count, _sum and _bucket series with vmrange labels in the same way
// as VictoriaMetrics converts native histograms received via Prometheus remote write protocol.
func testRemoteReadNativeHistograms(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier, newRemoteReadServer func(t *testing.T, series []*prompb.TimeSeries) *RemoteReadServer, vmctlFlags []string) {
t := tc.T()
t.Helper()
series := GenerateRemoteReadHistogramSeries(1749655870, 1749655880, 2)
rrs := newRemoteReadServer(t, series)
defer rrs.Close()
vmctlFlags = append(vmctlFlags, `--remote-read-src-addr=`+rrs.HTTPAddr())
tc.MustStartVmctl("vmctl", vmctlFlags)
sut.ForceFlush(t)
tc.Assert(&apptest.AssertOptions{
Retries: 300,
Msg: `unexpected native histogram metrics stored on vmsingle via the prometheus protocol`,
Got: func() any {
got := sut.PrometheusAPIV1Export(t, `{__name__=~".*"}`, apptest.QueryOpts{
Start: "2025-06-11T15:31:10Z",
End: "2025-06-11T15:32:20Z",
})
got.Sort()
return got.Data.Result
},
Want: expectedNativeHistogramQueryResult(),
CmpOpts: []cmp.Option{
cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType"),
},
})
}
// expectedNativeHistogramQueryResult returns the series expected to be stored in VictoriaMetrics
// after migrating the series generated by GenerateRemoteReadHistogramSeries(1749655870, 1749655880, 2).
func expectedNativeHistogramQueryResult() []*apptest.QueryResult {
metric := func(name, vmrange string) map[string]string {
m := map[string]string{
"__name__": name,
"job": "0",
}
if vmrange != "" {
m["vmrange"] = vmrange
}
return m
}
samples := func(v1, v2 float64) []*apptest.Sample {
return []*apptest.Sample{
{Timestamp: 1749655870000, Value: v1},
{Timestamp: 1749655875000, Value: v2},
}
}
resp := &apptest.PrometheusAPIV1QueryResponse{
Data: &apptest.QueryData{
Result: []*apptest.QueryResult{
{Metric: metric("vm_histogram_metric_count", ""), Samples: samples(10, 20)},
{Metric: metric("vm_histogram_metric_sum", ""), Samples: samples(25.5, 51)},
{Metric: metric("vm_histogram_metric_bucket", "-1.000e+00...-5.000e-01"), Samples: samples(4, 8)},
{Metric: metric("vm_histogram_metric_bucket", "-1.000e-03...1.000e-03"), Samples: samples(2, 4)},
{Metric: metric("vm_histogram_metric_bucket", "5.000e-01...1.000e+00"), Samples: samples(1, 2)},
{Metric: metric("vm_histogram_metric_bucket", "1.000e+00...2.000e+00"), Samples: samples(3, 6)},
},
},
}
// sort in the same way as the exported result
resp.Sort()
return resp.Data.Result
}
func testRemoteReadProtocol(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier, newRemoteReadServer func(t *testing.T) *RemoteReadServer, vmctlFlags []string) {
t := tc.T()
t.Helper()

View File

@@ -25,6 +25,7 @@
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": 3,
"links": [
{
"icon": "doc",

View File

@@ -5136,110 +5136,6 @@
],
"title": "Major page faults rate ($instance)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "$ds"
},
"description": "Average duration of fsync system calls. Spikes or high latency indicates that storage I/O cannot keep up with the write rate, which may slow down data ingestion.\n\nIf this value is elevated:\n- Check disk-related metrics.\n- If using cloud storage, check IOPS and throughput limits.\n- If using NFS, check network performance, server resource usage, and any errors on the NFS server side.\n- Check for known bugs in the filesystem or kernel version being used.",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 305
},
"id": 230,
"options": {
"legend": {
"calcs": [
"lastNotNull",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true,
"sortBy": "Last *",
"sortDesc": true
},
"tooltip": {
"hideZeros": true,
"mode": "multi",
"sort": "desc"
}
},
"pluginVersion": "12.2.0",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "$ds"
},
"editorMode": "code",
"expr": "rate(vm_filestream_fsync_duration_seconds_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval]) / rate(vm_filestream_fsync_calls_total{job=~\"$job_storage\", instance=~\"$instance\"}[$__rate_interval])",
"format": "time_series",
"instant": false,
"legendFormat": "{{instance}}",
"refId": "A"
}
],
"title": "Fsync avg duration ($instance)",
"type": "timeseries"
}
],
"title": "Troubleshooting",

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