Compare commits

..

2 Commits

Author SHA1 Message Date
hagen1778
4e0fd3143d go mod tidy
Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-09-11 15:28:09 +02:00
hagen1778
3ee6583267 vendor: bump VictoriaMetrics/metrics to v1.40.0
Addresses following issues:
* https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9161
* https://github.com/VictoriaMetrics/metrics/issues/98

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-09-11 15:26:01 +02:00
1480 changed files with 71737 additions and 86134 deletions

View File

@@ -49,14 +49,14 @@ jobs:
restore-keys: go-artifacts-${{ runner.os }}-codeql-analyze-
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: 'language:go'

View File

@@ -35,7 +35,7 @@ jobs:
uses: actions/checkout@v5
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '24.x'

View File

@@ -3,7 +3,7 @@
[![Latest Release](https://img.shields.io/github/v/release/VictoriaMetrics/VictoriaMetrics?sort=semver&label=&filter=!*-victorialogs&logo=github&labelColor=gray&color=gray&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Freleases%2Flatest)](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)
[![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)
[![Build Status](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml/badge.svg?branch=master&link=https%3A%2F%2Fgithub.com%2FVictoriaMetrics%2FVictoriaMetrics%2Factions)](https://github.com/VictoriaMetrics/VictoriaMetrics/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/branch/master/graph/badge.svg?link=https%3A%2F%2Fcodecov.io%2Fgh%2FVictoriaMetrics%2FVictoriaMetrics)](https://app.codecov.io/gh/VictoriaMetrics/VictoriaMetrics)
[![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)
![Slack](https://img.shields.io/badge/Join-4A154B?logo=slack&link=https%3A%2F%2Fslack.victoriametrics.com)

View File

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

View File

@@ -111,7 +111,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
flagutil.ApplySecretFlags()
remotewrite.InitSecretFlags()
buildinfo.Init()
logger.Init()

View File

@@ -2,7 +2,6 @@ package opentelemetry
import (
"fmt"
"io"
"net/http"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
@@ -25,13 +24,6 @@ var (
rowsPerInsert = metrics.NewHistogram(`vmagent_rows_per_insert{type="opentelemetry"}`)
)
// InsertHandler processes metrics from given reader.
func InsertHandlerForReader(at *auth.Token, r io.Reader, encoding string) error {
return stream.ParseStream(r, encoding, nil, func(tss []prompb.TimeSeries, mms []prompb.MetricMetadata) error {
return insertRows(at, tss, mms, nil)
})
}
// InsertHandler processes opentelemetry metrics.
func InsertHandler(at *auth.Token, req *http.Request) error {
extraLabels, err := protoparserutil.GetExtraLabels(req)

View File

@@ -31,7 +31,7 @@ type Group struct {
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5155
EvalDelay *promutil.Duration `yaml:"eval_delay,omitempty"`
Limit *int `yaml:"limit,omitempty"`
Limit int `yaml:"limit,omitempty"`
Rules []Rule `yaml:"rules"`
Concurrency int `yaml:"concurrency"`
// Labels is a set of label value pairs, that will be added to every rule.
@@ -91,8 +91,8 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
if g.EvalOffset != nil && g.EvalDelay != nil {
return fmt.Errorf("eval_offset cannot be used with eval_delay")
}
if g.Limit != nil && *g.Limit < 0 {
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", *g.Limit)
if g.Limit < 0 {
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", g.Limit)
}
if g.Concurrency < 0 {
return fmt.Errorf("invalid concurrency %d, shouldn't be less than 0", g.Concurrency)

View File

@@ -181,10 +181,9 @@ func TestGroupValidate_Failure(t *testing.T) {
EvalOffset: promutil.NewDuration(2 * time.Minute),
}, false, "eval_offset should be smaller than interval")
limit := -1
f(&Group{
Name: "wrong limit",
Limit: &limit,
Limit: -1,
}, false, "invalid limit")
f(&Group{

View File

@@ -173,10 +173,9 @@ func (c *Client) Query(ctx context.Context, query string, ts time.Time) (Result,
return Result{}, nil, fmt.Errorf("second attempt: %w", err)
}
}
defer func() { _ = resp.Body.Close() }()
// Process the received response.
var parseFn func(resp *http.Response) (Result, error)
var parseFn func(req *http.Request, resp *http.Response) (Result, error)
switch c.dataSourceType {
case datasourcePrometheus:
parseFn = parsePrometheusResponse
@@ -187,12 +186,9 @@ func (c *Client) Query(ctx context.Context, query string, ts time.Time) (Result,
default:
logger.Panicf("BUG: unsupported datasource type %q to parse query response", c.dataSourceType)
}
result, err := parseFn(resp)
if err != nil {
return Result{}, nil, fmt.Errorf("error parsing response from %q: %w", req.URL.Redacted(), err)
}
return result, req, nil
result, err := parseFn(req, resp)
_ = resp.Body.Close()
return result, req, err
}
// QueryRange executes the given query on the given time range.
@@ -233,10 +229,9 @@ func (c *Client) QueryRange(ctx context.Context, query string, start, end time.T
return res, fmt.Errorf("second attempt: %w", err)
}
}
defer func() { _ = resp.Body.Close() }()
// Process the received response.
var parseFn func(resp *http.Response) (Result, error)
var parseFn func(req *http.Request, resp *http.Response) (Result, error)
switch c.dataSourceType {
case datasourcePrometheus:
parseFn = parsePrometheusResponse
@@ -245,11 +240,8 @@ func (c *Client) QueryRange(ctx context.Context, query string, start, end time.T
default:
logger.Panicf("BUG: unsupported datasource type %q to parse query range response", c.dataSourceType)
}
res, err = parseFn(resp)
if err != nil {
return Result{}, fmt.Errorf("error parsing response from %q: %w", req.URL.Redacted(), err)
}
res, err = parseFn(req, resp)
_ = resp.Body.Close()
return res, err
}

View File

@@ -33,10 +33,10 @@ func (r graphiteResponse) metrics() []Metric {
return ms
}
func parseGraphiteResponse(resp *http.Response) (Result, error) {
func parseGraphiteResponse(req *http.Request, resp *http.Response) (Result, error) {
r := &graphiteResponse{}
if err := json.NewDecoder(resp.Body).Decode(r); err != nil {
return Result{}, fmt.Errorf("error parsing graphite metrics: %w", err)
return Result{}, fmt.Errorf("error parsing graphite metrics for %s: %w", req.URL.Redacted(), err)
}
return Result{Data: r.metrics()}, nil
}

View File

@@ -172,16 +172,16 @@ const (
rtVector, rtMatrix, rScalar = "vector", "matrix", "scalar"
)
func parsePrometheusResponse(resp *http.Response) (res Result, err error) {
func parsePrometheusResponse(req *http.Request, resp *http.Response) (res Result, err error) {
r := &promResponse{}
if err = json.NewDecoder(resp.Body).Decode(r); err != nil {
return res, fmt.Errorf("failed to decode response: %w", err)
return res, fmt.Errorf("error parsing response from %s: %w", req.URL.Redacted(), err)
}
if r.Status == statusError {
return res, fmt.Errorf("response error %q: %s", r.ErrorType, r.Error)
return res, fmt.Errorf("response error, query: %s, errorType: %s, error: %s", req.URL.Redacted(), r.ErrorType, r.Error)
}
if r.Status != statusSuccess {
return res, fmt.Errorf("unknown response status %q", r.Status)
return res, fmt.Errorf("unknown status: %s, Expected success or error", r.Status)
}
var parseFn func() ([]Metric, error)
switch r.Data.ResultType {

View File

@@ -135,7 +135,7 @@ func TestVMInstantQuery(t *testing.T) {
expErr(vmQuery, "500") // 0
expErr(vmQuery, "error parsing response") // 1
expErr(vmQuery, "response error") // 2
expErr(vmQuery, "unknown response status") // 3
expErr(vmQuery, "unknown status") // 3
expErr(vmQuery, "unexpected end of JSON input") // 4
res, _, err := pq.Query(ctx, vmQuery, ts) // 5 - vector

View File

@@ -40,8 +40,8 @@ func (c *Client) setVLogsRangeReqParams(r *http.Request, query string, start, en
c.setReqParams(r, query)
}
func parseVLogsResponse(resp *http.Response) (res Result, err error) {
res, err = parsePrometheusResponse(resp)
func parseVLogsResponse(req *http.Request, resp *http.Response) (res Result, err error) {
res, err = parsePrometheusResponse(req, resp)
if err != nil {
return Result{}, err
}

View File

@@ -90,7 +90,6 @@ func main() {
flag.CommandLine.SetOutput(os.Stdout)
flag.Usage = usage
envflag.Parse()
flagutil.ApplySecretFlags()
remoteread.InitSecretFlags()
remotewrite.InitSecretFlags()
datasource.InitSecretFlags()

View File

@@ -389,7 +389,7 @@ func (ar *AlertingRule) execRange(ctx context.Context, start, end time.Time) ([]
return []datasource.Metric{{Timestamps: []int64{0}, Values: []float64{math.NaN()}}}, nil
}
for _, s := range res.Data {
ls, err := ar.expandLabelTemplates(s, qFn)
ls, err := ar.expandLabelTemplates(s)
if err != nil {
return nil, err
}
@@ -482,7 +482,7 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
expandedLabels := make([]*labelSet, len(res.Data))
expandedAnnotations := make([]map[string]string, len(res.Data))
for i, m := range res.Data {
ls, err := ar.expandLabelTemplates(m, qFn)
ls, err := ar.expandLabelTemplates(m)
if err != nil {
curState.Err = err
return nil, curState.Err
@@ -604,7 +604,10 @@ func (ar *AlertingRule) exec(ctx context.Context, ts time.Time, limit int) ([]pr
return append(tss, ar.toTimeSeries(ts.Unix())...), nil
}
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric, qFn templates.QueryFn) (*labelSet, error) {
func (ar *AlertingRule) expandLabelTemplates(m datasource.Metric) (*labelSet, error) {
qFn := func(_ string) ([]datasource.Metric, error) {
return nil, fmt.Errorf("`query` template isn't supported in rule label")
}
ls, err := ar.toLabels(m, qFn)
if err != nil {
return nil, fmt.Errorf("failed to expand label templates: %s", err)

View File

@@ -10,7 +10,6 @@ import (
"strings"
"sync"
"testing"
"testing/synctest"
"time"
"github.com/VictoriaMetrics/metrics"
@@ -1430,142 +1429,3 @@ func TestAlertingRuleExec_Partial(t *testing.T) {
t.Fatalf("unexpected error: %s", err)
}
}
func TestAlertingRule_QueryTemplateInLabels(t *testing.T) {
fq := &datasource.FakeQuerier{}
fakeGroup := Group{
Name: "TestQueryTemplateInLabels",
}
ar := &AlertingRule{
Name: "test_alert",
Labels: map[string]string{
"suppress_for_mass_alert": `{{ if (printf "ALERTS{alertname='SomeAlert', alertstate='firing', device='%s'} == 1" $labels.device | query) }}true{{ else }}false{{ end }}`,
},
Annotations: map[string]string{
"summary": "Test alert with query template in labels",
},
alerts: make(map[uint64]*notifier.Alert),
}
ar.GroupID = fakeGroup.GetID()
ar.q = fq
ar.state = &ruleState{
entries: make([]StateEntry, 10),
}
// Add a metric that should trigger the alert
fq.Add(metricWithValueAndLabels(t, 1, "device", "sda1"))
ts := time.Now()
_, err := ar.exec(context.TODO(), ts, 0)
if err != nil {
t.Fatalf("unexpected error with query template in labels: %s", err)
}
// Verify that the alert was created and the query template was executed
if len(ar.alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(ar.alerts))
}
alert := ar.GetAlerts()[0]
suppressLabel, exists := alert.Labels["suppress_for_mass_alert"]
if !exists {
t.Fatalf("expected 'suppress_for_mass_alert' label to exist")
}
// The query template should have been executed (even if it returns false due to mock data)
if suppressLabel != "true" && suppressLabel != "false" {
t.Fatalf("expected 'suppress_for_mass_alert' label to be 'true' or 'false', got '%s'", suppressLabel)
}
}
// TestAlertingRule_ActiveAtPreservedInAnnotations ensures that the fix for
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9543 is preserved
// while allowing query templates in labels (https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9783)
func TestAlertingRule_ActiveAtPreservedInAnnotations(t *testing.T) {
// wrap into synctest because of time manipulations
synctest.Test(t, func(t *testing.T) {
fq := &datasource.FakeQuerier{}
ar := &AlertingRule{
Name: "TestActiveAtPreservation",
Labels: map[string]string{
"test_query_in_label": `{{ "static_value" }}`,
},
Annotations: map[string]string{
"description": "Alert active since {{ $activeAt }}",
},
alerts: make(map[uint64]*notifier.Alert),
q: fq,
state: &ruleState{
entries: make([]StateEntry, 10),
},
}
// Mock query result - return empty result to make suppress_for_mass_alert = false
// (no need to add anything to fq for empty result)
// Add a metric that should trigger the alert
fq.Add(metricWithValueAndLabels(t, 1, "instance", "server1"))
// First execution - creates new alert
ts1 := time.Now()
_, err := ar.exec(context.TODO(), ts1, 0)
if err != nil {
t.Fatalf("unexpected error on first exec: %s", err)
}
if len(ar.alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(ar.alerts))
}
firstAlert := ar.GetAlerts()[0]
// Verify first execution: activeAt should be ts1 and annotation should reflect it
if !firstAlert.ActiveAt.Equal(ts1) {
t.Fatalf("expected activeAt to be %v, got %v", ts1, firstAlert.ActiveAt)
}
// Extract time from annotation (format will be like "Alert active since 2025-09-30 08:55:13.638551611 -0400 EDT m=+0.002928464")
expectedTimeStr := ts1.Format("2006-01-02 15:04:05")
if !strings.Contains(firstAlert.Annotations["description"], expectedTimeStr) {
t.Fatalf("first exec annotation should contain time %s, got: %s", expectedTimeStr, firstAlert.Annotations["description"])
}
// Second execution - should preserve activeAt in annotation
// Ensure different timestamp with different seconds
// sleep is non-blocking thanks to synctest
time.Sleep(2 * time.Second)
ts2 := time.Now()
_, err = ar.exec(context.TODO(), ts2, 0)
if err != nil {
t.Fatalf("unexpected error on second exec: %s", err)
}
// Get the alert again (should be the same alert)
if len(ar.alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(ar.alerts))
}
secondAlert := ar.GetAlerts()[0]
// Critical test: activeAt should still be ts1, not ts2
if !secondAlert.ActiveAt.Equal(ts1) {
t.Fatalf("activeAt should be preserved as %v, but got %v", ts1, secondAlert.ActiveAt)
}
// Critical test: annotation should still contain ts1 time, not ts2
if !strings.Contains(secondAlert.Annotations["description"], expectedTimeStr) {
t.Fatalf("second exec annotation should still contain original time %s, got: %s", expectedTimeStr, secondAlert.Annotations["description"])
}
// Additional verification: annotation should NOT contain ts2 time
ts2TimeStr := ts2.Format("2006-01-02 15:04:05")
if strings.Contains(secondAlert.Annotations["description"], ts2TimeStr) {
t.Fatalf("annotation should NOT contain new eval time %s, got: %s", ts2TimeStr, secondAlert.Annotations["description"])
}
// Verify query template in labels still works (this would fail if query templates were broken)
if firstAlert.Labels["test_query_in_label"] != "static_value" {
t.Fatalf("expected test_query_in_label=static_value, got %s", firstAlert.Labels["test_query_in_label"])
}
})
}

View File

@@ -24,10 +24,6 @@ import (
)
var (
ruleResultsLimit = flag.Int("rule.resultsLimit", 0, "Limits the number of alerts or recording results a single rule can produce. "+
"Can be overridden by the limit option under group if specified. "+
"If exceeded, the rule will be marked with an error and all its results will be discarded. "+
"0 means no limit.")
ruleUpdateEntriesLimit = flag.Int("rule.updateEntriesLimit", 20, "Defines the max number of rule's state updates stored in-memory. "+
"Rule's updates are available on rule's Details page and are used for debugging purposes. The number of stored updates can be overridden per rule via update_entries_limit param.")
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier.")
@@ -115,6 +111,7 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
Name: cfg.Name,
File: cfg.File,
Interval: cfg.Interval.Duration(),
Limit: cfg.Limit,
Concurrency: cfg.Concurrency,
checksum: cfg.Checksum,
Params: cfg.Params,
@@ -131,11 +128,6 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
if g.Interval == 0 {
g.Interval = defaultInterval
}
if cfg.Limit != nil {
g.Limit = *cfg.Limit
} else {
g.Limit = *ruleResultsLimit
}
if g.Concurrency < 1 {
g.Concurrency = 1
}

View File

@@ -372,54 +372,20 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
updateHeadersByConfig(w.Header(), hc.ResponseHeaders)
w.WriteHeader(res.StatusCode)
err = copyStreamToClient(w, res.Body)
copyBuf := copyBufPool.Get()
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
_, err = io.CopyBuffer(w, res.Body, copyBuf.B)
copyBufPool.Put(copyBuf)
_ = res.Body.Close()
if err != nil && !netutil.IsTrivialNetworkError(err) && !errors.Is(err, context.Canceled) {
if err != nil && !netutil.IsTrivialNetworkError(err) {
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
requestURI := httpserver.GetRequestURI(r)
logger.Warnf("remoteAddr: %s; requestURI: %s; error when proxying response body from %s: %s", remoteAddr, requestURI, targetURL, err)
return true, false
}
return true, false
}
func copyStreamToClient(client io.Writer, backend io.Reader) error {
copyBuf := copyBufPool.Get()
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
defer copyBufPool.Put(copyBuf)
buf := copyBuf.B
flusher, ok := client.(http.Flusher)
if !ok {
logger.Panicf("BUG: client must implement net/http.Flusher interface; got %T", client)
}
for {
n, backendErr := backend.Read(buf)
if n > 0 {
data := buf[:n]
n, clientErr := client.Write(data)
if clientErr != nil {
return fmt.Errorf("cannot write data to client: %w", clientErr)
}
if n != len(data) {
logger.Panicf("BUG: unexpected number of bytes written returned by client.Write; got %d; want %d", n, len(data))
}
// Flush the read data from the backend to the client as fast as possible
// in order to reduce delays for data propagation.
// See https://github.com/VictoriaMetrics/VictoriaLogs/issues/667
flusher.Flush()
}
if backendErr != nil {
if backendErr == io.EOF {
return nil
}
return fmt.Errorf("cannot read data from backend: %w", backendErr)
}
}
}
var copyBufPool bytesutil.ByteBufferPool
func copyHeader(dst, src http.Header) {

View File

@@ -514,11 +514,6 @@ func (w *fakeResponseWriter) getResponse() string {
return w.bb.String()
}
// Flush implements net/http.Flusher
func (w *fakeResponseWriter) Flush() {
// Nothing to do.
}
func (w *fakeResponseWriter) Header() http.Header {
if w.h == nil {
w.h = http.Header{}

View File

@@ -115,7 +115,7 @@ func main() {
if err != nil {
logger.Fatalf("cannot create backup: %s", err)
}
pushmetrics.StopAndPush()
pushmetrics.Stop()
startTime := time.Now()
logger.Infof("gracefully shutting down http server for metrics at %q", listenAddrs)

View File

@@ -68,7 +68,7 @@ func main() {
if err := a.Run(ctx); err != nil {
logger.Fatalf("cannot restore from backup: %s", err)
}
pushmetrics.StopAndPush()
pushmetrics.Stop()
srcFS.MustStop()
dstFS.MustStop()

View File

@@ -197,13 +197,13 @@ func newNextSeriesForSearchQuery(ec *evalConfig, sq *storage.SearchQuery, expr g
}
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
t := timerpool.Get(30 * time.Second)
defer timerpool.Put(t)
select {
case seriesCh <- s:
case <-t.C:
logger.Errorf("resource leak when processing the %s (full query: %s); please report this error to VictoriaMetrics developers",
expr.AppendString(nil), ec.originalQuery)
}
timerpool.Put(t)
return nil
})
close(seriesCh)

View File

@@ -1150,23 +1150,15 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
}
qt.Printf("optimized calculation for instant rollup avg_over_time(m[d]) as (sum_over_time(m[d]) / count_over_time(m[d]))")
fe := expr.(*metricsql.FuncExpr)
// copy RollupExpr to drop possible offset,
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
newArg.Offset = nil
feSum := *fe
feSum.Name = "sum_over_time"
feCount := *fe
feCount.Name = "count_over_time"
be := &metricsql.BinaryOpExpr{
Op: "/",
KeepMetricNames: fe.KeepMetricNames,
Left: &metricsql.FuncExpr{
Name: "sum_over_time",
Args: []metricsql.Expr{newArg},
KeepMetricNames: fe.KeepMetricNames,
},
Right: &metricsql.FuncExpr{
Name: "count_over_time",
Args: []metricsql.Expr{newArg},
KeepMetricNames: fe.KeepMetricNames,
},
Left: &feSum,
Right: &feCount,
}
return evalExpr(qt, ec, be)
case "rate":
@@ -1180,12 +1172,8 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
fe := afe.Args[0].(*metricsql.FuncExpr)
feIncrease := *fe
feIncrease.Name = "increase"
// copy RollupExpr to drop possible offset,
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
newArg.Offset = nil
feIncrease.Args = []metricsql.Expr{newArg}
d := newArg.Window.Duration(ec.Step)
re := fe.Args[0].(*metricsql.RollupExpr)
d := re.Window.Duration(ec.Step)
if d == 0 {
d = ec.Step
}
@@ -1205,12 +1193,8 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
fe := expr.(*metricsql.FuncExpr)
feIncrease := *fe
feIncrease.Name = "increase"
// copy RollupExpr to drop possible offset,
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
newArg.Offset = nil
feIncrease.Args = []metricsql.Expr{newArg}
d := newArg.Window.Duration(ec.Step)
re := fe.Args[0].(*metricsql.RollupExpr)
d := re.Window.Duration(ec.Step)
if d == 0 {
d = ec.Step
}
@@ -2015,23 +1999,3 @@ func dropStaleNaNs(funcName string, values []float64, timestamps []int64) ([]flo
}
return dstValues, dstTimestamps
}
func copyRollupExpr(re *metricsql.RollupExpr) *metricsql.RollupExpr {
var newRe metricsql.RollupExpr
newRe.Expr = re.Expr
newRe.InheritStep = re.InheritStep
newRe.At = re.At
if re.Window != nil {
newRe.Window = &metricsql.DurationExpr{}
*newRe.Window = *re.Window
}
if re.Offset != nil {
newRe.Offset = &metricsql.DurationExpr{}
*newRe.Offset = *re.Offset
}
if re.Step != nil {
newRe.Step = &metricsql.DurationExpr{}
*newRe.Step = *re.Step
}
return &newRe
}

View File

@@ -123,7 +123,7 @@ The list of MetricsQL features on top of PromQL:
* `if` binary operator. `q1 if q2` removes values from `q1` for missing values from `q2`.
* `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for existing values from `q2`.
* `WITH` templates. This feature simplifies writing and managing complex queries.
Go to [WITH templates playground](https://play.victoriametrics.com/select/0/prometheus/graph/#/expand-with-exprs) and try it.
Go to [WITH templates playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) and try it.
* String literals may be concatenated. This is useful with `WITH` templates:
`WITH (commonPrefix="long_metric_prefix_") {__name__=commonPrefix+"suffix1"} / {__name__=commonPrefix+"suffix2"}`.
* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions), [transform functions](#transform-functions)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,6 @@
<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"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/>
@@ -37,10 +36,10 @@
<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-D13qGB62.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DY9kCvzk.js">
<script type="module" crossorigin src="./assets/index-DK22yiEQ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-I8MVeF75.css">
<link rel="stylesheet" crossorigin href="./assets/index-Ccv_zSYG.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,4 +1,4 @@
FROM golang:1.25.3 AS build-web-stage
FROM golang:1.25.0 AS build-web-stage
COPY build /build
WORKDIR /build
@@ -6,7 +6,7 @@ COPY web/ /build/
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o web-amd64 github.com/VictoriMetrics/vmui/ && \
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o web-windows github.com/VictoriMetrics/vmui/
FROM alpine:3.22.2
FROM alpine:3.22.1
USER root
COPY --from=build-web-stage /build/web-amd64 /app/web

View File

@@ -79,13 +79,15 @@ export default [...compat.extends(
}],
"react/jsx-first-prop-new-line": [1, "multiline"],
"object-curly-spacing": [2, "always"],
// Disable core indent rule due to recursion issues in ESLint 9; use JSX-specific rules instead
indent: "off",
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
indent: ["error", 2, {
SwitchCase: 1,
}],
// Formatting rules moved out of ESLint core; omit here to avoid deprecation noise
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"react/prop-types": 0,
"react/react-in-jsx-scope": "off",

View File

@@ -6,7 +6,6 @@
<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"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/>

View File

@@ -17,7 +17,7 @@
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.6.3",
"uplot": "^1.6.32",
"vite": "^7.1.5",
"vite": "^7.0.4",
"web-vitals": "^5.0.3"
},
"devDependencies": {
@@ -7321,13 +7321,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
@@ -7337,13 +7337,10 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -7354,9 +7351,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -7660,17 +7657,17 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"fdir": "^6.4.6",
"picomatch": "^4.0.2",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
"rollup": "^4.40.0",
"tinyglobby": "^0.2.14"
},
"bin": {
"vite": "bin/vite.js"
@@ -7775,13 +7772,10 @@
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -7792,9 +7786,9 @@
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"

View File

@@ -29,7 +29,7 @@
"react-input-mask": "^2.0.4",
"react-router-dom": "^7.6.3",
"uplot": "^1.6.32",
"vite": "^7.1.5",
"vite": "^7.0.4",
"web-vitals": "^5.0.3"
},
"devDependencies": {

View File

@@ -123,7 +123,7 @@ The list of MetricsQL features on top of PromQL:
* `if` binary operator. `q1 if q2` removes values from `q1` for missing values from `q2`.
* `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for existing values from `q2`.
* `WITH` templates. This feature simplifies writing and managing complex queries.
Go to [WITH templates playground](https://play.victoriametrics.com/select/0/prometheus/graph/#/expand-with-exprs) and try it.
Go to [WITH templates playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) and try it.
* String literals may be concatenated. This is useful with `WITH` templates:
`WITH (commonPrefix="long_metric_prefix_") {__name__=commonPrefix+"suffix1"} / {__name__=commonPrefix+"suffix2"}`.
* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions), [transform functions](#transform-functions)

View File

@@ -116,7 +116,7 @@ const LegendConfigs: FC<Props> = ({ data, isCompact }) => {
onEnter={onApplyFormat}
/>
<span className="vm-legend-configs-item__info vm-legend-configs-item__info_input">
Customize legend labels with text and &#123;&#123;label_name&#125;&#125; placeholders.
Customize legend labels with text and &#123;&#123;label_name&#125;&#125; placeholders.
</span>
</div>
@@ -130,7 +130,7 @@ const LegendConfigs: FC<Props> = ({ data, isCompact }) => {
searchable
/>
<span className="vm-legend-configs-item__info">
Choose a label to group the legend. By default, legends are grouped by query.
Choose a label to group the legend. By default, legends are grouped by query.
</span>
</div>
</>

View File

@@ -142,7 +142,7 @@ const StepConfigurator: FC = () => {
startIcon={<TimelineIcon/>}
onClick={toggleOpenOptions}
>
Step: {isAutoStep ? `auto (${customStep})` : customStep}
Step: {isAutoStep ? `auto (${customStep})` : customStep}
</Button>
)}
<Popper

View File

@@ -9,7 +9,6 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface NotifiersHeaderProps {
kinds: string[];
allKinds: string[];
search: string;
onChangeKinds: (input: string) => void;
onChangeSearch: (input: string) => void;
}
@@ -17,7 +16,6 @@ interface NotifiersHeaderProps {
const NotifiersHeader: FC<NotifiersHeaderProps> = ({
kinds,
allKinds,
search,
onChangeKinds,
onChangeSearch,
}) => {
@@ -48,7 +46,6 @@ const NotifiersHeader: FC<NotifiersHeaderProps> = ({
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"
value={search}
placeholder="Filter by kind, address or labels"
startIcon={<SearchIcon />}
onChange={onChangeSearch}

View File

@@ -11,7 +11,6 @@ interface RulesHeaderProps {
allTypes: string[];
allStates: string[];
states: string[];
search: string;
onChangeTypes: (input: string) => void;
onChangeStates: (input: string) => void;
onChangeSearch: (input: string) => void;
@@ -22,7 +21,6 @@ const RulesHeader: FC<RulesHeaderProps> = ({
allTypes,
allStates,
states,
search,
onChangeTypes,
onChangeStates,
onChangeSearch,
@@ -71,7 +69,6 @@ const RulesHeader: FC<RulesHeaderProps> = ({
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"
value={search}
placeholder="Filter by rule, name or labels"
startIcon={<SearchIcon />}
onChange={onChangeSearch}

View File

@@ -106,7 +106,7 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
onClick={handleClickRemove}
fullWidth
>
Remove graph
Remove graph
</Button>
</div>
</Modal>

View File

@@ -13,8 +13,6 @@ import Button from "../../Button/Button";
interface DatePickerProps {
date: Date | Dayjs
format?: string
minDate?: Date | Dayjs
maxDate?: Date | Dayjs
onChange: (date: string) => void
}
@@ -26,8 +24,6 @@ enum CalendarTypeView {
const Calendar: FC<DatePickerProps> = ({
date,
minDate,
maxDate,
format = DATE_TIME_FORMAT,
onChange,
}) => {
@@ -38,8 +34,6 @@ const Calendar: FC<DatePickerProps> = ({
const today = dayjs.tz();
const viewDateIsToday = today.format(DATE_FORMAT) === viewDate.format(DATE_FORMAT);
const { isMobile } = useDeviceDetect();
const min = minDate ? dayjs(minDate) : undefined;
const max = maxDate ? dayjs(maxDate) : undefined;
const toggleDisplayYears = () => {
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.days : CalendarTypeView.years);
@@ -81,13 +75,9 @@ const Calendar: FC<DatePickerProps> = ({
onChangeViewDate={handleChangeViewDate}
toggleDisplayYears={toggleDisplayYears}
showArrowNav={viewType === CalendarTypeView.days}
hasPrev={viewType === CalendarTypeView.days && (!min || viewDate.startOf("month").isAfter(min))}
hasNext={viewType === CalendarTypeView.days && (!max || viewDate.endOf("month").isBefore(max))}
/>
{viewType === CalendarTypeView.days && (
<CalendarBody
minDate={min}
maxDate={max}
viewDate={viewDate}
selectDate={selectDate}
onChangeSelectDate={handleChangeSelectDate}
@@ -95,16 +85,12 @@ const Calendar: FC<DatePickerProps> = ({
)}
{viewType === CalendarTypeView.years && (
<YearsList
minDate={min}
maxDate={max}
viewDate={viewDate}
onChangeViewDate={handleChangeViewDate}
/>
)}
{viewType === CalendarTypeView.months && (
<MonthsList
minDate={min}
maxDate={max}
selectDate={selectDate}
viewDate={viewDate}
onChangeViewDate={handleChangeViewDate}
@@ -117,7 +103,7 @@ const Calendar: FC<DatePickerProps> = ({
size="small"
onClick={handleToday}
>
show today
show today
</Button>
</div>
)}

View File

@@ -4,8 +4,6 @@ import classNames from "classnames";
import Tooltip from "../../../Tooltip/Tooltip";
interface CalendarBodyProps {
minDate?: Dayjs
maxDate?: Dayjs
viewDate: Dayjs
selectDate: Dayjs
onChangeSelectDate: (date: Dayjs) => void
@@ -13,7 +11,7 @@ interface CalendarBodyProps {
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const CalendarBody: FC<CalendarBodyProps> = ({ minDate, maxDate, viewDate: date, selectDate, onChangeSelectDate }) => {
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate: date, selectDate, onChangeSelectDate }) => {
const format = "YYYY-MM-DD";
const today = dayjs.tz();
const viewDate = dayjs(date.format(format));
@@ -46,25 +44,21 @@ const CalendarBody: FC<CalendarBodyProps> = ({ minDate, maxDate, viewDate: date,
</Tooltip>
))}
{days.map((d, i) => {
const isDisabled = d && ((minDate && d.isBefore(minDate)) || (maxDate && d.isAfter(maxDate)));
return (
<div
className={classNames({
"vm-calendar-body-cell": true,
"vm-calendar-body-cell_day": true,
"vm-calendar-body-cell_day_empty": !d,
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format),
"vm-calendar-body-cell_day_disabled": isDisabled,
})}
key={d ? d.format(format) : i}
onClick={isDisabled ? undefined : createHandlerSelectDate(d)}
>
{d && d.format("D")}
</div>
);
})}
{days.map((d, i) => (
<div
className={classNames({
"vm-calendar-body-cell": true,
"vm-calendar-body-cell_day": true,
"vm-calendar-body-cell_day_empty": !d,
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format)
})}
key={d ? d.format(format) : i}
onClick={createHandlerSelectDate(d)}
>
{d && d.format("D")}
</div>
))}
</div>
);
};

View File

@@ -1,18 +1,15 @@
import { FC } from "preact/compat";
import { Dayjs } from "dayjs";
import { ArrowDownIcon, ArrowDropDownIcon } from "../../../Icons";
import classNames from "classnames";
interface CalendarHeaderProps {
viewDate: Dayjs
onChangeViewDate: (date: Dayjs) => void
showArrowNav: boolean
toggleDisplayYears: () => void
hasNext: boolean
hasPrev: boolean
}
const CalendarHeader: FC<CalendarHeaderProps> = ({ hasPrev, hasNext, viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
const setPrevMonth = () => {
onChangeViewDate(viewDate.subtract(1, "month"));
@@ -38,20 +35,14 @@ const CalendarHeader: FC<CalendarHeaderProps> = ({ hasPrev, hasNext, viewDate, s
{showArrowNav && (
<div className="vm-calendar-header-right">
<div
className={classNames({
"vm-calendar-header-right__prev": true,
"vm-calendar-header-right_disabled": !hasPrev,
})}
onClick={hasPrev ? setPrevMonth : undefined}
className="vm-calendar-header-right__prev"
onClick={setPrevMonth}
>
<ArrowDownIcon/>
</div>
<div
className={classNames({
"vm-calendar-header-right__next": true,
"vm-calendar-header-right_disabled": !hasNext,
})}
onClick={hasNext ? setNextMonth : undefined}
className="vm-calendar-header-right__next"
onClick={setNextMonth}
>
<ArrowDownIcon/>
</div>

View File

@@ -3,14 +3,13 @@ import dayjs, { Dayjs } from "dayjs";
import classNames from "classnames";
interface CalendarMonthsProps {
minDate?: Dayjs
maxDate?: Dayjs
viewDate: Dayjs,
selectDate: Dayjs
onChangeViewDate: (date: Dayjs) => void
}
const MonthsList: FC<CalendarMonthsProps> = ({ minDate, maxDate, viewDate, selectDate, onChangeViewDate }) => {
const MonthsList: FC<CalendarMonthsProps> = ({ viewDate, selectDate, onChangeViewDate }) => {
const today = dayjs().format("MM");
const currentMonths = useMemo(() => selectDate.format("MM"), [selectDate]);
@@ -30,24 +29,20 @@ const MonthsList: FC<CalendarMonthsProps> = ({ minDate, maxDate, viewDate, selec
return (
<div className="vm-calendar-years">
{months.map(m => {
const isDisabled = m && ((minDate && m.isBefore(minDate)) || (maxDate && m.isAfter(maxDate)));
return (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
"vm-calendar-years__year_today": m.format("MM") === today,
"vm-calendar-years__year_disabled": isDisabled,
})}
id={`vm-calendar-year-${m.format("MM")}`}
key={m.format("MM")}
onClick={isDisabled ? undefined : createHandlerClick(m)}
>
{m.format("MMMM")}
</div>
);
})}
{months.map(m => (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
"vm-calendar-years__year_today": m.format("MM") === today
})}
id={`vm-calendar-year-${m.format("MM")}`}
key={m.format("MM")}
onClick={createHandlerClick(m)}
>
{m.format("MMMM")}
</div>
))}
</div>
);
};

View File

@@ -3,13 +3,11 @@ import dayjs, { Dayjs } from "dayjs";
import classNames from "classnames";
interface CalendarYearsProps {
minDate?: Dayjs
maxDate?: Dayjs
viewDate: Dayjs
onChangeViewDate: (date: Dayjs) => void
}
const YearsList: FC<CalendarYearsProps> = ({ minDate, maxDate, viewDate, onChangeViewDate }) => {
const YearsList: FC<CalendarYearsProps> = ({ viewDate, onChangeViewDate }) => {
const today = dayjs().format("YYYY");
const currentYear = useMemo(() => viewDate.format("YYYY"), [viewDate]);
@@ -32,24 +30,20 @@ const YearsList: FC<CalendarYearsProps> = ({ minDate, maxDate, viewDate, onChang
return (
<div className="vm-calendar-years">
{years.map(y => {
const isDisabled = y && (minDate && y.isBefore(minDate)) || (maxDate && y.isAfter(maxDate));
return (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
"vm-calendar-years__year_today": y.format("YYYY") === today,
"vm-calendar-years__year_disabled": isDisabled,
})}
id={`vm-calendar-year-${y.format("YYYY")}`}
key={y.format("YYYY")}
onClick={isDisabled ? undefined : createHandlerClick(y)}
>
{y.format("YYYY")}
</div>
);
})}
{years.map(y => (
<div
className={classNames({
"vm-calendar-years__year": true,
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
"vm-calendar-years__year_today": y.format("YYYY") === today
})}
id={`vm-calendar-year-${y.format("YYYY")}`}
key={y.format("YYYY")}
onClick={createHandlerClick(y)}
>
{y.format("YYYY")}
</div>
))}
</div>
);
};

View File

@@ -69,10 +69,6 @@
}
}
&_disabled {
color: $color-text-disabled;
}
&__prev {
transform: rotate(90deg);
}
@@ -112,12 +108,7 @@
cursor: pointer;
transition: color 200ms ease, background-color 300ms ease-in-out;
&_disabled {
cursor: unset;
color: $color-text-disabled;
}
&:not(&_disabled):hover {
&:hover {
background-color: $color-hover-black;
}
@@ -157,12 +148,7 @@
cursor: pointer;
transition: color 200ms ease, background-color 300ms ease-in-out;
&_disabled {
cursor: unset;
color: $color-text-disabled;
}
&:not(&_disabled):hover {
&:hover {
background-color: $color-hover-black;
}

View File

@@ -8,13 +8,11 @@ import useBoolean from "../../../hooks/useBoolean";
import useEventListener from "../../../hooks/useEventListener";
interface DatePickerProps {
date: string | Date | Dayjs;
date: string | Date | Dayjs,
targetRef: React.RefObject<HTMLElement>;
format?: string;
label?: string;
minDate?: Date | Dayjs;
maxDate?: Date | Dayjs;
onChange: (val: string) => void;
format?: string
label?: string
onChange: (val: string) => void
}
const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
@@ -22,9 +20,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
targetRef,
format = DATE_TIME_FORMAT,
onChange,
label,
minDate,
maxDate
label
}, ref) => {
const dateDayjs = useMemo(() => dayjs(date).isValid() ? dayjs.tz(date) : dayjs().tz(), [date]);
const { isMobile } = useDeviceDetect();
@@ -60,8 +56,6 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
date={dateDayjs}
format={format}
onChange={handleChangeDate}
minDate={minDate}
maxDate={maxDate}
/>
</div>
</Popper>

View File

@@ -3,52 +3,39 @@ import { ChangeEvent, KeyboardEvent } from "react";
import { CalendarIcon } from "../../Icons";
import DatePicker from "../DatePicker";
import Button from "../../Button/Button";
import { DATE_ISO_FORMAT, DATE_FORMAT, DATE_TIME_FORMAT } from "../../../../constants/date";
import { DATE_TIME_FORMAT } from "../../../../constants/date";
import InputMask from "react-input-mask";
import dayjs, { Dayjs } from "dayjs";
import dayjs from "dayjs";
import classNames from "classnames";
import "./style.scss";
const formatStringDate = (val: string, format: string) => {
return dayjs(val).isValid() ? dayjs.tz(val).format(format) : val;
const formatStringDate = (val: string) => {
return dayjs(val).isValid() ? dayjs.tz(val).format(DATE_TIME_FORMAT) : val;
};
interface DateTimeInputProps {
value?: string;
label: string;
pickerLabel: string;
format?: string;
dateOnly?: boolean;
pickerRef: React.RefObject<HTMLDivElement>;
onChange: (date: string) => void;
onEnter: () => void;
disabled?: boolean;
minDate?: Date | Dayjs;
maxDate?: Date | Dayjs;
}
const masks: Record<string, string> = {
[DATE_ISO_FORMAT]: "9999-99-99T99:99:99",
[DATE_FORMAT]: "9999-99-99",
[DATE_TIME_FORMAT]: "9999-99-99 99:99:99"
};
const DateTimeInput: FC<DateTimeInputProps> = ({
value = "",
format = DATE_TIME_FORMAT,
minDate,
maxDate,
dateOnly = false,
label,
pickerLabel,
pickerRef,
onChange,
onEnter,
disabled
onEnter
}) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
const mask = masks[format];
const [maskedValue, setMaskedValue] = useState(formatStringDate(value, format));
const [maskedValue, setMaskedValue] = useState(formatStringDate(value));
const [focusToTime, setFocusToTime] = useState(false);
const [awaitChangeForEnter, setAwaitChangeForEnter] = useState(false);
const error = dayjs(maskedValue).isValid() ? "" : "Invalid date format";
@@ -68,13 +55,16 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
}
};
const mask = dateOnly ? "9999-99-99" : "9999-99-99 99:99:99";
const placeholder = dateOnly ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm:ss";
const handleChangeDate = (val: string) => {
setMaskedValue(val);
setFocusToTime(true);
};
useEffect(() => {
const newValue = formatStringDate(value, format);
const newValue = formatStringDate(value);
if (newValue !== maskedValue) {
setMaskedValue(newValue);
}
@@ -97,8 +87,7 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
<div
className={classNames({
"vm-date-time-input": true,
"vm-date-time-input_error": error,
"vm-date-time-input_disabled": disabled,
"vm-date-time-input_error": error
})}
>
<label>{label}</label>
@@ -106,7 +95,7 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
tabIndex={1}
inputRef={setInputRef}
mask={mask}
placeholder={format}
placeholder={placeholder}
value={maskedValue}
autoCapitalize={"none"}
inputMode={"numeric"}
@@ -114,7 +103,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
onChange={handleMaskedChange}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
disabled={disabled}
/>
{error && (
<span className="vm-date-time-input__error-text">{error}</span>
@@ -129,7 +117,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
size="small"
startIcon={<CalendarIcon/>}
ariaLabel="calendar"
disabled={disabled}
/>
</div>
<DatePicker
@@ -138,9 +125,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
date={maskedValue}
onChange={handleChangeDate}
targetRef={wrapperRef}
minDate={minDate}
maxDate={maxDate}
format={format}
/>
</div>
);

View File

@@ -23,14 +23,6 @@
user-select: none;
}
&_disabled {
cursor: default;
pointer-events: none;
* {
color: $color-text-disabled !important;
}
}
&__icon {
position: absolute;
bottom: 2px;

View File

@@ -46,12 +46,11 @@ const Select: FC<SelectProps> = ({
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
const [wrapperRef, setWrapperRef] = useState<React.RefObject<HTMLElement> | null>(null);
const [openList, setOpenList] = useState(false);
const resultList = [...list];
const inputRef = useRef<HTMLInputElement>(null);
const isMultiple = Array.isArray(value);
let selectedValues = Array.isArray(value) ? value.slice() : [];
const selectedValues = Array.isArray(value) ? value.slice() : [];
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
const textFieldValue = useMemo(() => {
@@ -78,7 +77,7 @@ const Select: FC<SelectProps> = ({
};
const handleBlur = () => {
resultList.includes(search) && onChange(search);
list.includes(search) && onChange(search);
};
const handleToggleList = (e: MouseEvent<HTMLDivElement>) => {
@@ -124,10 +123,8 @@ const Select: FC<SelectProps> = ({
useEventListener("keyup", handleKeyUp);
useClickOutside(autocompleteAnchorEl, handleCloseList, wrapperRef);
if (includeAll && !resultList.includes("All")) resultList.push("All");
if (includeAll && (!selectedValues?.length || selectedValues?.length === resultList?.length)) {
selectedValues = ["All"];
}
includeAll && !list.includes("All") && list.push("All");
includeAll && !selectedValues?.length && selectedValues.push("All");
return (
<div
@@ -158,7 +155,6 @@ const Select: FC<SelectProps> = ({
onInput={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
ref={inputRef}
readOnly={isMobile || !searchable}
/>
@@ -186,7 +182,7 @@ const Select: FC<SelectProps> = ({
itemClassName={itemClassName}
label={label}
value={autocompleteValue}
options={resultList.map(l => ({ value: l }))}
options={list.map(l => ({ value: l }))}
anchor={autocompleteAnchorEl}
selected={selectedValues}
minLength={1}

View File

@@ -128,14 +128,8 @@
}
&_disabled {
pointer-events: none;
* {
color: var(--color-text-disabled);
cursor: default;
}
input::placeholder {
color: var(--color-text-disabled);
cursor: not-allowed;
}
.vm-select-input {

View File

@@ -72,6 +72,7 @@ const TextField: FC<TextFieldProps> = ({
"vm-text-field__input_error": error,
"vm-text-field__input_warning": !error && warning,
"vm-text-field__input_icon-start": startIcon,
"vm-text-field__input_disabled": disabled,
"vm-text-field__input_textarea": type === "textarea",
});
@@ -135,14 +136,12 @@ const TextField: FC<TextFieldProps> = ({
className={classNames({
"vm-text-field": true,
"vm-text-field_textarea": type === "textarea",
"vm-text-field_dark": isDarkTheme,
"vm-text-field_disabled": disabled
"vm-text-field_dark": isDarkTheme
})}
data-replicated-value={value}
>
{startIcon && <div className="vm-text-field__icon-start">{startIcon}</div>}
{endIcon && <div className="vm-text-field__icon-end">{endIcon}</div>}
{label && <span className="vm-text-field__label">{label}</span>}
{type === "textarea"
? (
<textarea
@@ -181,6 +180,7 @@ const TextField: FC<TextFieldProps> = ({
/>
)
}
{label && <span className="vm-text-field__label">{label}</span>}
<TextFieldMessage
error={error}
warning={warning}

View File

@@ -1,19 +1,11 @@
@use "src/styles/variables" as *;
.vm-text-field {
position: relative;
display: grid;
margin: 6px 0;
width: 100%;
&_disabled {
color: $color-text-disabled;
pointer-events: none;
}
&:is(&_disabled) > &__label {
color: $color-text-disabled;
}
&_textarea:after {
content: attr(data-replicated-value) " ";
white-space: pre-wrap;
@@ -30,7 +22,7 @@
background-color: transparent;
font-size: $font-size;
line-height: 18px;
grid-area: 2 / 1 / 2 / 2;
grid-area: 1 / 1 / 2 / 2;
overflow: hidden;
box-sizing: border-box;
}
@@ -39,10 +31,8 @@
&__error,
&__warning,
&__helper-text, {
width: fit-content;
margin-top: calc(($font-size-small/-2) - 1px);
margin-bottom: calc(($font-size-small/-2) - 1px);
margin-left: calc($padding-global/2);
position: absolute;
left: calc($padding-global/2);
max-width: calc(100% - $padding-global);
padding: 0 3px;
font-size: $font-size-small;
@@ -60,12 +50,16 @@
}
&__label {
top: calc(($font-size-small/-2) - 2px);
color: $color-text-secondary;
}
&__helper-text,
&__warning,
&__error {
position: relative;
top: calc($font-size-small/-2);
width: fit-content;
overflow-wrap: anywhere;
pointer-events: auto;
user-select: text;
@@ -149,17 +143,28 @@
&__icon-start,
&__icon-end {
align-self: center;
display: flex;
align-items: center;
justify-content: center;
max-width: 15px;
top: 0;
left: $padding-small;
height: 36px;
position: absolute;
color: $color-text-secondary;
}
&__icon-start {
left: $padding-small;
}
&__icon-end {
left: auto;
right: $padding-small;
}
&__controls-info {
position: absolute;
bottom: $padding-small;
right: $padding-global;
color: $color-text-secondary;
font-size: $font-size-small;
opacity: 0.8;
}
}

View File

@@ -198,7 +198,7 @@ const TableSettings: FC<TableSettingsProps> = ({
{toggleTableCompact && tableCompact !== undefined && (
<div className="vm-table-settings-modal-section">
<div className="vm-table-settings-modal-section__title">
Table view
Table view
</div>
<div className="vm-table-settings-modal-columns-list__item">
<Switch

View File

@@ -75,7 +75,7 @@ const TracingsView: FC<TraceViewProps> = ({ traces, jsonEditor = false, onDelete
>
<div className="vm-tracings-view-trace-header">
<h3 className="vm-tracings-view-trace-header-title">
Trace for <b className="vm-tracings-view-trace-header-title__query">{trace.queryValue}</b>
Trace for <b className="vm-tracings-view-trace-header-title__query">{trace.queryValue}</b>
</h3>
<Tooltip title={expandedTraces.includes(trace.idValue) ? "Collapse All" : "Expand All"}>
<Button

View File

@@ -128,7 +128,7 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
>
<ArrowDropDownIcon/>
</div>
Value
Value
</div>
</td>
{hasCopyValue && <td className="vm-table-cell vm-table-cell_header"/>}

View File

@@ -45,7 +45,7 @@ export const lightPalette = {
"color-success": "#4caf50",
"color-background-success": "#d4ecd5",
"color-passive": "#5d6267",
"color-code": "#ecedee",
"color-code": "ecedee",
"color-background-body": "#FEFEFF",
"color-background-block": "#FFFFFF",
"color-background-tooltip": "rgba(80,80,80,0.9)",

View File

@@ -61,7 +61,7 @@ const DebugInfoClipboardApi = () => (
target="_blank"
rel="noopener noreferrer"
>
Clipboard API documentation
Clipboard API documentation
</a>
</p>
</p>

View File

@@ -1,39 +1,42 @@
import { useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useCallback } from "preact/compat";
type ParamValue = string | number | boolean | null | undefined;
const useSearchParamsFromObject = () => {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, ParamValue>) => {
const hadParams = !!searchParams.size;
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, string | number>) => {
const hasSearchParams = !!searchParams.size;
let hasChanged = false;
const newSearchParams = new URLSearchParams(searchParams);
const beforeParams = searchParams.toString();
for (const [key, newValue] of Object.entries(objectParams)) {
const isEmpty = newValue === null || newValue === undefined || newValue === "";
if (isEmpty) {
searchParams.keys().forEach(key => {
if (!(key in objectParams)) {
newSearchParams.delete(key);
continue;
hasChanged = true;
}
});
const next = String(newValue);
if (newSearchParams.get(key) !== next) {
newSearchParams.set(key, next);
Object.entries(objectParams).forEach(([key, value]) => {
if (newSearchParams.get(key) !== `${value}`) {
newSearchParams.set(key, `${value}`);
hasChanged = true;
}
});
if (!hasChanged) return;
if (hasSearchParams) {
setSearchParams(newSearchParams);
} else {
navigate(`?${newSearchParams.toString()}`, { replace: true });
}
}, [searchParams, navigate]);
if (beforeParams === newSearchParams.toString()) return;
setSearchParams(newSearchParams, { replace: !hadParams });
},
[searchParams, setSearchParams]
);
return { setSearchParamsFromKeys };
return {
setSearchParamsFromKeys
};
};
export default useSearchParamsFromObject;

View File

@@ -67,27 +67,39 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
})}
style={{ background, color }}
>
<div
className={classNames({
"vm-header-logo": true,
"vm-header-logo_mobile": displaySidebar,
})}
onClick={onClickLogo}
style={{ color }}
>
{<Logo/>}
</div>
{displaySidebar ? (
<SidebarHeader
background={background}
color={color}
/>
) : (
<HeaderNav
color={color}
background={background}
/>
<>
{!appModeEnable && (
<div
className="vm-header-logo"
onClick={onClickLogo}
style={{ color }}
>
{<Logo/>}
</div>
)}
<HeaderNav
color={color}
background={background}
/>
</>
)}
{displaySidebar && (
<div
className={classNames({
"vm-header-logo": true,
"vm-header-logo_mobile": true,
})}
onClick={onClickLogo}
style={{ color }}
>
{<Logo/>}
</div>
)}
<HeaderControls
controlsComponent={controlsComponent}

View File

@@ -53,7 +53,7 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
if (isMobile) {
return (
<>
<div className="vm-header-controls vm-header-controls_mobile">
<div className="vm-header-controls">
<Button
className={classNames({
"vm-header-button": !appModeEnable

View File

@@ -1,7 +1,6 @@
@use "src/styles/variables" as *;
.vm-header-controls {
order: 3;
display: flex;
align-items: center;
justify-content: flex-end;
@@ -12,7 +11,6 @@
display: grid;
grid-template-columns: 1fr;
padding: 0;
flex-grow: initial;
.vm-header-button {
border: none;

View File

@@ -3,7 +3,6 @@
$sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
.vm-header-sidebar {
order: 1;
width: 24px;
height: 24px;
color: inherit;
@@ -17,7 +16,7 @@ $sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
left: 0;
top: 0;
height: 51px;
width: 48px;
width: 51px;
transition: left 350ms $sidebar-transition;
&_open {
@@ -54,6 +53,15 @@ $sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1);
transform: translateX(0);
}
&__logo {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
width: 65px;
}
&-settings {
display: grid;
align-items: center;

View File

@@ -17,6 +17,7 @@
@media (max-width: 1000px) {
position: sticky;
top: 0;
gap: $padding-small;
padding: $padding-small;
}
@@ -52,7 +53,6 @@
}
&_mobile {
order: 2;
max-width: 75px;
min-width: 75px;
margin: 0 auto;

View File

@@ -13,8 +13,6 @@ import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject"
import useStateSearchParams from "../../../hooks/useStateSearchParams";
import Hyperlink from "../../../components/Main/Hyperlink/Hyperlink";
const DEFAULT_TOP_N = 10;
const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isCluster, ...props }) => {
const { isMobile } = useDeviceDetect();
const [searchParams] = useSearchParams();
@@ -23,8 +21,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
const showTips = searchParams.get("tips") || "";
const [match, setMatch] = useStateSearchParams("", "match");
const [focusLabel, setFocusLabel] = useStateSearchParams("", "focusLabel");
const [topN, setTopN] = useStateSearchParams(DEFAULT_TOP_N, "topN");
const hasChanges = !!(match || focusLabel || (topN !== DEFAULT_TOP_N && !isPrometheus));
const [topN, setTopN] = useStateSearchParams(10, "topN");
const errorTopN = useMemo(() => topN < 0 ? "Number must be bigger than zero" : "", [topN]);
@@ -38,10 +35,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
};
const handleResetQuery = () => {
setSearchParamsFromKeys({ match: "", focusLabel: "", topN: "" });
setMatch("");
setFocusLabel("");
setTopN(DEFAULT_TOP_N);
setSearchParamsFromKeys({ match: "", focusLabel: "" });
};
const handleToggleTips = () => {
@@ -51,7 +45,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
useEffect(() => {
const matchQuery = searchParams.get("match");
const topNQuery = +(searchParams.get("topN") || DEFAULT_TOP_N);
const topNQuery = +(searchParams.get("topN") || 10);
const focusLabelQuery = searchParams.get("focusLabel");
if (matchQuery !== match) setMatch(matchQuery || "");
if (topNQuery !== topN) setTopN(topNQuery);
@@ -100,7 +94,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
<TextField
label="Limit entries"
type="number"
value={isPrometheus ? DEFAULT_TOP_N : topN}
value={isPrometheus ? 10 : topN}
error={errorTopN}
disabled={isPrometheus}
helperText={isPrometheus ? "not available for Prometheus" : ""}
@@ -122,7 +116,7 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
withIcon={true}
>
<WikiIcon/>
Statistic inaccuracy explanation
Statistic inaccuracy explanation
</Hyperlink>
</div>
}
@@ -151,9 +145,8 @@ const CardinalityConfigurator: FC<CardinalityTotalsProps> = ({ isPrometheus, isC
variant="text"
startIcon={<RestartIcon/>}
onClick={handleResetQuery}
disabled={!hasChanges}
>
Reset filters
Reset
</Button>
<Button
startIcon={<PlayIcon/>}

View File

@@ -5,7 +5,6 @@ import usePrevious from "../../../hooks/usePrevious";
import { MAX_QUERY_FIELDS } from "../../../constants/graph";
import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext";
import { useTimeDispatch } from "../../../state/time/TimeStateContext";
import { getQueryStringValue } from "../../../utils/query-string";
import {
DeleteIcon,
PlayIcon,
@@ -22,7 +21,6 @@ import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import { arrayEquals } from "../../../utils/array";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import { QueryStats } from "../../../api/types";
import { usePrettifyQuery } from "./hooks/usePrettifyQuery";
import QueryHistory from "../../../components/QueryHistory/QueryHistory";
@@ -52,9 +50,6 @@ export interface QueryConfiguratorProps {
}
}
const defaultHideQueryStr = getQueryStringValue("expr.hide", "") as string;
const defaultHideQuery: number[] = defaultHideQueryStr.split(",").filter(v => v).map(Number);
const QueryConfigurator: FC<QueryConfiguratorProps> = ({
queryErrors,
setQueryErrors,
@@ -74,10 +69,9 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
const { query, queryHistory, autocomplete, autocompleteQuick } = useQueryState();
const queryDispatch = useQueryDispatch();
const timeDispatch = useTimeDispatch();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [stateQuery, setStateQuery] = useState(query || []);
const [hideQuery, setHideQuery] = useState<number[]>(defaultHideQuery);
const [hideQuery, setHideQuery] = useState<number[]>([]);
const [awaitStateQuery, setAwaitStateQuery] = useState(false);
const prevStateQuery = usePrevious(stateQuery) as (undefined | string[]);
@@ -182,7 +176,6 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({
useEffect(() => {
onHideQuery && onHideQuery(hideQuery);
setSearchParamsFromKeys({ "expr.hide": hideQuery.join(",") });
}, [hideQuery]);
useEffect(() => {

View File

@@ -17,8 +17,8 @@ const WarningHeatmapToLine:FC = () => {
<Alert variant="warning">
<div className="vm-warning-heatmap-to-line">
<p className="vm-warning-heatmap-to-line__text">
The expression cannot be displayed as a heatmap.
To make the graph work, disable the heatmap in the &quot;Graph settings&quot; or modify the expression.
The expression cannot be displayed as a heatmap.
To make the graph work, disable the heatmap in the &quot;Graph settings&quot; or modify the expression.
</p>
<Button

View File

@@ -40,7 +40,7 @@ const WarningLimitSeries: FC<Props> = ({ warning, query, onChange }) => {
variant="outlined"
onClick={handleShowAll}
>
Show all
Show all
</Button>
</div>
</Alert>

View File

@@ -58,11 +58,6 @@ export const useSetQueryParams = () => {
newSearchParams.set(`${group}.relative_time`, relativeTime || "none");
}
const exprHide = searchParams.get("expr.hide") || "";
if (exprHide !== "") {
newSearchParams.set("expr.hide", exprHide);
}
const stepFromUrl = searchParams.get(`${group}.step_input`) || step;
if (stepFromUrl && (stepFromUrl !== customStep)) {
newSearchParams.set(`${group}.step_input`, customStep);

View File

@@ -105,7 +105,6 @@ const ExploreNotifiers: FC = () => {
<NotifiersHeader
kinds={kinds}
allKinds={Array.from(allKinds)}
search={searchInput}
onChangeKinds={handleChangeKinds}
onChangeSearch={debounce(handleChangeSearch, 500)}
/>

View File

@@ -90,6 +90,14 @@ const ExploreRules: FC = () => {
}
};
const handleChangeStates = useCallback((title: string) => {
setStates(getChanges(title, states));
}, [states]);
const handleChangeTypes = useCallback((title: string) => {
setTypes(getChanges(title, types));
}, [types]);
const noRuleFound = "No rules found!";
const handleClose = (id: string) => {
@@ -147,35 +155,16 @@ const ExploreRules: FC = () => {
[groups, types, states, searchInput]
);
if (!types.every(v => allTypes.has(v))) {
setTypes([]);
}
const selectedTypes = allTypes.size === types.length ? [] : types;
if (!states.every(v => allStates.has(v))) {
setStates([]);
}
const selectedStates = allStates.size === states.length ? [] : states;
const handleChangeStates = useCallback((title: string) => {
setStates(getChanges(title, selectedStates));
}, [states]);
const handleChangeTypes = useCallback((title: string) => {
setTypes(getChanges(title, selectedTypes));
}, [types]);
return (
<>
{modalOpen && getModal()}
{(!modalOpen || !!allStates?.size) && (
<div className="vm-explore-alerts">
<RulesHeader
types={selectedTypes}
types={types}
allTypes={Array.from(allTypes)}
states={selectedStates}
states={states}
allStates={Array.from(allStates)}
search={searchInput}
onChangeTypes={handleChangeTypes}
onChangeStates={handleChangeStates}
onChangeSearch={debounce(handleChangeSearch, 500)}

View File

@@ -1,26 +0,0 @@
import { describe, it, expect } from "vitest";
import { getDefaultURL } from "./default-server-url";
describe("test server urls", () => {
describe("getDefaultURL()", () => {
it("/select/0/vmui/", () => {
const result = getDefaultURL("https://localhost:1111/select/0/vmui/");
expect(result).toBe("https://localhost:1111/select/0/prometheus");
});
it("/any/path/prefix/select/multitenant/vmui/#/rules?q=test", () => {
const result = getDefaultURL("http://test/any/path/prefix/select/multitenant/vmui/#/rules?q=test");
expect(result).toBe("http://test/any/path/prefix/select/multitenant/prometheus");
});
it("/test/select/1:1/prometheus/graph/", () => {
const result = getDefaultURL("https://domain.com/test/select/1:1/prometheus/graph/");
expect(result).toBe("https://domain.com/test/select/1:1/prometheus");
});
it("https://play.vm.com/#/rules?q=test", () => {
const result = getDefaultURL("https://play.vm.com/#/rules?q=test");
expect(result).toBe("https://play.vm.com");
});
});
});

View File

@@ -3,15 +3,12 @@ import { replaceTenantId } from "./tenants";
import { APP_TYPE, AppType } from "../constants/appType";
import { getFromStorage } from "./storage";
export const getDefaultURL = (u: string) => {
return u.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "").replace(/(\/select\/[^/]+)$/, "$1/prometheus");
};
export const getDefaultServer = (tenantId?: string): string => {
const { serverURL } = getAppModeParams();
const storageURL = getFromStorage("SERVER_URL") as string;
const anomalyURL = `${window.location.origin}${window.location.pathname.replace(/^\/vmui/, "")}`;
const defaultURL = getDefaultURL(window.location.href);
const baseURL = window.location.href.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "");
const defaultURL = baseURL.replace(/(\/select\/[\d:]+)$/, "$1/prometheus");
const url = serverURL || storageURL || defaultURL;
switch (APP_TYPE) {

View File

@@ -1,4 +1,4 @@
const regexp = /(\/select\/)([^/])(\/)(.+)/;
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
export const replaceTenantId = (serverUrl: string, tenantId: string) => {
return serverUrl.replace(regexp, `$1${tenantId}/$4`);

View File

@@ -19,8 +19,8 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
});
},
},
"/vmui/config.json": {
target: "https://play.victoriametrics.com/select/0",
"/flags": {
target: "https://play.victoriametrics.com",
changeOrigin: true,
configure: (proxy) => {
proxy.on("error", (err) => {

View File

@@ -1,8 +1,11 @@
package apptest
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/url"
"slices"
@@ -500,3 +503,44 @@ func sortTSDBStatusResponseEntries(entries []TSDBStatusResponseEntry) {
return left.Count < right.Count
})
}
// LogsQLQueryResponse is an in-memory representation of the
// /select/logsql/query response.
type LogsQLQueryResponse struct {
LogLines []string
}
// NewLogsQLQueryResponse is a test helper function that creates a new
// instance of LogsQLQueryResponse by unmarshalling a json string.
func NewLogsQLQueryResponse(t *testing.T, s string) *LogsQLQueryResponse {
t.Helper()
res := &LogsQLQueryResponse{}
if len(s) == 0 {
return res
}
bs := bytes.NewBufferString(s)
for {
logLine, err := bs.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
if len(logLine) > 0 {
t.Fatalf("BUG: unexpected non-empty line=%q with io.EOF", logLine)
}
break
}
t.Fatalf("BUG: cannot read logline from buffer: %s", err)
}
var lv map[string]any
if err := json.Unmarshal([]byte(logLine), &lv); err != nil {
t.Fatalf("cannot parse log line=%q: %s", logLine, err)
}
delete(lv, "_stream_id")
normalizedLine, err := json.Marshal(lv)
if err != nil {
t.Fatalf("cannot marshal parsed logline=%q: %s", logLine, err)
}
res.LogLines = append(res.LogLines, string(normalizedLine))
}
return res
}

View File

@@ -52,7 +52,6 @@ func testSpecialQueryRegression(tc *apptest.TestCase, sut apptest.PrometheusWrit
testTooBigLookbehindWindow(tc, sut)
testMatchSeries(tc, sut)
testNegativeIncrease(tc, sut)
testInstantQueryWithOffsetUsingCache(tc, sut)
// graphite
testComparisonNotInfNotNan(tc, sut)
@@ -293,45 +292,6 @@ func testNegativeIncrease(tc *apptest.TestCase, sut apptest.PrometheusWriteQueri
})
}
func testInstantQueryWithOffsetUsingCache(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier) {
t := tc.T()
// unexpected /api/v1/query response due to wrong applied offset to request range
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
sut.PrometheusAPIV1ImportPrometheus(t, []string{
`vm_http_requests_total 1 1758196800000`, // 2025-09-18 12:00:00
`vm_http_requests_total 2 1758218400000`, // 2025-09-18 18:00:00
`vm_http_requests_total 3 1758240000000`, // 2025-09-19 00:00:00
`vm_http_requests_total 4 1758261600000`, // 2025-09-19 06:00:00
`vm_http_requests_total 5 1758283200000`, // 2025-09-19 12:00:00
`vm_http_requests_total 6 1758304800000`, // 2025-09-19 18:00:00
`vm_http_requests_total 7 1758326400000`, // 2025-09-20 00:00:00
}, apptest.QueryOpts{})
sut.ForceFlush(t)
tc.Assert(&apptest.AssertOptions{
Msg: "unexpected /api/v1/query response",
DoNotRetry: true,
Got: func() any {
return sut.PrometheusAPIV1Query(t, `avg_over_time(vm_http_requests_total[1d] offset 12h)`, apptest.QueryOpts{
Time: "2025-09-20T12:00:01.000Z",
})
},
Want: &apptest.PrometheusAPIV1QueryResponse{
Status: "success",
Data: &apptest.QueryData{
ResultType: "vector",
Result: []*apptest.QueryResult{
{
Metric: map[string]string{},
Sample: &apptest.Sample{Timestamp: 1758369601000, Value: 5.5},
},
},
},
},
})
}
func testComparisonNotInfNotNan(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier) {
t := tc.T()

View File

@@ -1786,4 +1786,4 @@
"uid": "gF-lxRdVz",
"version": 1,
"weekStart": ""
}
}

View File

@@ -1168,7 +1168,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (le, controller) )",
"expr": "histogram_quantile(0.99,sum(rate(controller_runtime_reconcile_time_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by(le,controller) )",
"legendFormat": "q.99 {{controller}}",
"range": true,
"refId": "A"
@@ -1265,7 +1265,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "sum(rate(rest_client_requests_total{job=~\"$job\"}[$__interval])) by (method, code)",
"expr": "sum(rate(rest_client_requests_total{job=~\"$job\"}[$__interval])) by (method,code)",
"instant": false,
"legendFormat": "{{method}} {{code}}",
"range": true,
@@ -1489,7 +1489,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "max(histogram_quantile(0.99, sum(rate(go_sched_latencies_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (job, instance, le))) by (job)",
"expr": "max(histogram_quantile(0.99, sum(rate(go_sched_latencies_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (job, instance, le))) by(job)",
"instant": false,
"legendFormat": "__auto",
"range": true,
@@ -1588,7 +1588,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(rest_client_request_duration_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (le, method, api))",
"expr": "histogram_quantile(0.99,sum(rate(rest_client_request_duration_seconds_bucket{job=~\"$job\"})) by(le,method,api) )",
"instant": false,
"legendFormat": "{{method}} {{api}}",
"range": true,
@@ -2135,16 +2135,6 @@
"skipUrlSync": false,
"sort": 2,
"type": "query"
},
{
"baseFilters": [],
"datasource": {
"type": "prometheus",
"uid": "$ds"
},
"filters": [],
"name": "adhoc",
"type": "adhoc"
}
]
},

View File

@@ -1950,16 +1950,6 @@
],
"query": "*",
"type": "textbox"
},
{
"baseFilters": [],
"datasource": {
"type": "victoriametrics-logs-datasource",
"uid": "$ds"
},
"filters": [],
"name": "adhoc",
"type": "adhoc"
}
]
},
@@ -1972,4 +1962,4 @@
"title": "Query Stats (cluster)",
"uid": "feg3od1zt1fy8e",
"version": 1
}
}

View File

@@ -5739,7 +5739,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "min((vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without(type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type)\n )\n) > 0)",
"expr": "min((vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -10294,7 +10294,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "(vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without(type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type)\n )\n) > 0",
"expr": "(vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0",
"format": "time_series",
"interval": "",
"intervalFactor": 1,

View File

@@ -4991,7 +4991,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "(vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job\", instance=~\"$instance\"}[1d])) without(type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type)\n )\n) > 0",
"expr": "(vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0",
"format": "time_series",
"hide": false,
"interval": "",
@@ -6764,4 +6764,4 @@
"uid": "wNf0q_kZk",
"version": 1,
"weekStart": ""
}
}

View File

@@ -1787,4 +1787,4 @@
"uid": "gF-lxRdVz_vm",
"version": 1,
"weekStart": ""
}
}

View File

@@ -1994,7 +1994,7 @@
"baseFilters": [],
"datasource": {
"type": "victoriametrics-metrics-datasource",
"uid": "$ds"
"uid": "PE8D8DB4BEE4E4B22"
},
"filters": [],
"name": "adhoc",

View File

@@ -2136,16 +2136,6 @@
"skipUrlSync": false,
"sort": 2,
"type": "query"
},
{
"baseFilters": [],
"datasource": {
"type": "victoriametrics-metrics-datasource",
"uid": "$ds"
},
"filters": [],
"name": "adhoc",
"type": "adhoc"
}
]
},

View File

@@ -5739,7 +5739,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "min((vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without(type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type)\n )\n) > 0)",
"expr": "min((vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0)",
"format": "time_series",
"interval": "",
"intervalFactor": 1,
@@ -10294,7 +10294,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "(vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without(type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) \n )\n) > 0",
"expr": "(vm_free_disk_space_bytes{job=~\"$job_storage\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job_storage\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job_storage\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job_storage\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0",
"format": "time_series",
"interval": "",
"intervalFactor": 1,

View File

@@ -4992,7 +4992,7 @@
"uid": "$ds"
},
"editorMode": "code",
"expr": "(vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job\", instance=~\"$instance\"}[1d])) without(type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"}) without(type)\n )\n) > 0",
"expr": "(vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"}-vm_free_disk_space_limit_bytes{job=~\"$job\", instance=~\"$instance\"}) \n/ \nignoring(path) (\n (rate(vm_rows_added_to_storage_total{job=~\"$job\", instance=~\"$instance\"}[1d]) - \n sum(rate(vm_deduplicated_samples_total{job=~\"$job\", instance=~\"$instance\"}[1d])) without (type)) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type!~\"indexdb.*\"}) without(type)\n )\n +\n rate(vm_new_timeseries_created_total{job=~\"$job\", instance=~\"$instance\"}[1d]) * \n (\n sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"}) /\n sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type=\"indexdb/file\"})\n )\n) > 0",
"format": "time_series",
"hide": false,
"interval": "",
@@ -6765,4 +6765,4 @@
"uid": "wNf0q_kZk_vm",
"version": 1,
"weekStart": ""
}
}

View File

@@ -4238,4 +4238,4 @@
"title": "VictoriaMetrics - vmalert (VM)",
"uid": "LzldHAVnz_vm",
"version": 1
}
}

View File

@@ -2652,7 +2652,7 @@
{
"datasource": {
"type": "victoriametrics-datasource",
"uid": "$ds"
"uid": "P38648FE0F8C5BEA2"
},
"filters": [],
"hide": 0,

View File

@@ -3,11 +3,11 @@
DOCKER_REGISTRIES ?= docker.io quay.io
DOCKER_NAMESPACE ?= victoriametrics
ROOT_IMAGE ?= alpine:3.22.2
ROOT_IMAGE ?= alpine:3.22.1
ROOT_IMAGE_SCRATCH ?= scratch
CERTS_IMAGE := alpine:3.22.2
CERTS_IMAGE := alpine:3.22.1
GO_BUILDER_IMAGE := golang:1.25.3
GO_BUILDER_IMAGE := golang:1.25.0
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
BASE_IMAGE := local/base:1.1.4-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)

View File

@@ -3,7 +3,7 @@ services:
# It scrapes targets defined in --promscrape.config
# And forward them to --remoteWrite.url
vmagent:
image: victoriametrics/vmagent:v1.127.0
image: victoriametrics/vmagent:v1.125.1
depends_on:
- "vmauth"
ports:
@@ -19,7 +19,7 @@ services:
restart: always
grafana:
image: grafana/grafana:12.2.0
image: grafana/grafana:12.1.1
depends_on:
- "vmauth"
ports:
@@ -37,14 +37,14 @@ services:
# vmstorage shards. Each shard receives 1/N of all metrics sent to vminserts,
# where N is number of vmstorages (2 in this case).
vmstorage-1:
image: victoriametrics/vmstorage:v1.127.0-cluster
image: victoriametrics/vmstorage:v1.125.1-cluster
volumes:
- strgdata-1:/storage
command:
- "--storageDataPath=/storage"
restart: always
vmstorage-2:
image: victoriametrics/vmstorage:v1.127.0-cluster
image: victoriametrics/vmstorage:v1.125.1-cluster
volumes:
- strgdata-2:/storage
command:
@@ -54,7 +54,7 @@ services:
# vminsert is ingestion frontend. It receives metrics pushed by vmagent,
# pre-process them and distributes across configured vmstorage shards.
vminsert-1:
image: victoriametrics/vminsert:v1.127.0-cluster
image: victoriametrics/vminsert:v1.125.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -63,7 +63,7 @@ services:
- "--storageNode=vmstorage-2:8400"
restart: always
vminsert-2:
image: victoriametrics/vminsert:v1.127.0-cluster
image: victoriametrics/vminsert:v1.125.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -75,7 +75,7 @@ services:
# vmselect is a query fronted. It serves read queries in MetricsQL or PromQL.
# vmselect collects results from configured `--storageNode` shards.
vmselect-1:
image: victoriametrics/vmselect:v1.127.0-cluster
image: victoriametrics/vmselect:v1.125.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -85,7 +85,7 @@ services:
- "--vmalert.proxyURL=http://vmalert:8880"
restart: always
vmselect-2:
image: victoriametrics/vmselect:v1.127.0-cluster
image: victoriametrics/vmselect:v1.125.1-cluster
depends_on:
- "vmstorage-1"
- "vmstorage-2"
@@ -100,7 +100,7 @@ services:
# read requests from Grafana, vmui, vmalert among vmselects.
# It can be used as an authentication proxy.
vmauth:
image: victoriametrics/vmauth:v1.127.0
image: victoriametrics/vmauth:v1.125.1
depends_on:
- "vmselect-1"
- "vmselect-2"
@@ -114,7 +114,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
image: victoriametrics/vmalert:v1.127.0
image: victoriametrics/vmalert:v1.125.1
depends_on:
- "vmauth"
ports:
@@ -138,7 +138,7 @@ services:
# alertmanager receives alerting notifications from vmalert
# and distributes them according to --config.file.
alertmanager:
image: prom/alertmanager:v0.28.1
image: prom/alertmanager:v0.28.0
volumes:
- ./alertmanager.yml:/config/alertmanager.yml
command:

View File

@@ -3,7 +3,7 @@ services:
# It scrapes targets defined in --promscrape.config
# And forward them to --remoteWrite.url
vmagent:
image: victoriametrics/vmagent:v1.127.0
image: victoriametrics/vmagent:v1.125.1
depends_on:
- "victoriametrics"
ports:
@@ -18,7 +18,7 @@ services:
# VictoriaMetrics instance, a single process responsible for
# storing metrics and serve read requests.
victoriametrics:
image: victoriametrics/victoria-metrics:v1.127.0
image: victoriametrics/victoria-metrics:v1.125.1
ports:
- 8428:8428
- 8089:8089
@@ -38,7 +38,7 @@ services:
restart: always
grafana:
image: grafana/grafana:12.2.0
image: grafana/grafana:12.1.1
depends_on:
- "victoriametrics"
ports:
@@ -54,7 +54,7 @@ services:
# vmalert executes alerting and recording rules
vmalert:
image: victoriametrics/vmalert:v1.127.0
image: victoriametrics/vmalert:v1.125.1
depends_on:
- "victoriametrics"
- "alertmanager"
@@ -79,7 +79,7 @@ services:
# alertmanager receives alerting notifications from vmalert
# and distributes them according to --config.file.
alertmanager:
image: prom/alertmanager:v0.28.1
image: prom/alertmanager:v0.28.0
volumes:
- ./alertmanager.yml:/config/alertmanager.yml
command:

View File

@@ -13,14 +13,14 @@ groups:
expr: |
sum(vm_free_disk_space_bytes) without(path) /
(
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without(type)) * (
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without (type)) * (
sum(vm_data_size_bytes{type!~"indexdb.*"}) without(type) /
sum(vm_rows{type!~"indexdb.*"}) without(type)
)
+
rate(vm_new_timeseries_created_total[1d]) * (
sum(vm_data_size_bytes{type="indexdb/file"}) without(type) /
sum(vm_rows{type="indexdb/file"}) without(type)
sum(vm_data_size_bytes{type="indexdb/file"}) /
sum(vm_rows{type="indexdb/file"})
)
) < 3 * 24 * 3600 > 0
for: 30m
@@ -37,14 +37,14 @@ groups:
expr: |
sum(vm_free_disk_space_bytes - vm_free_disk_space_limit_bytes) without(path) /
(
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without(type)) * (
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without (type)) * (
sum(vm_data_size_bytes{type!~"indexdb.*"}) without(type) /
sum(vm_rows{type!~"indexdb.*"}) without(type)
)
+
rate(vm_new_timeseries_created_total[1d]) * (
sum(vm_data_size_bytes{type="indexdb/file"}) without(type) /
sum(vm_rows{type="indexdb/file"}) without(type)
sum(vm_data_size_bytes{type="indexdb/file"}) /
sum(vm_rows{type="indexdb/file"})
)
) < 3 * 24 * 3600 > 0
for: 30m

View File

@@ -13,14 +13,14 @@ groups:
expr: |
sum(vm_free_disk_space_bytes) without(path) /
(
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without(type)) * (
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without (type)) * (
sum(vm_data_size_bytes{type!~"indexdb.*"}) without(type) /
sum(vm_rows{type!~"indexdb.*"}) without(type)
)
+
rate(vm_new_timeseries_created_total[1d]) * (
sum(vm_data_size_bytes{type="indexdb/file"}) without(type)/
sum(vm_rows{type="indexdb/file"}) without(type)
sum(vm_data_size_bytes{type="indexdb/file"}) /
sum(vm_rows{type="indexdb/file"})
)
) < 3 * 24 * 3600 > 0
for: 30m
@@ -37,14 +37,14 @@ groups:
expr: |
sum(vm_free_disk_space_bytes - vm_free_disk_space_limit_bytes) without(path) /
(
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without(type)) * (
(rate(vm_rows_added_to_storage_total[1d]) - sum(rate(vm_deduplicated_samples_total[1d])) without (type)) * (
sum(vm_data_size_bytes{type!~"indexdb.*"}) without(type) /
sum(vm_rows{type!~"indexdb.*"}) without(type)
)
+
rate(vm_new_timeseries_created_total[1d]) * (
sum(vm_data_size_bytes{type="indexdb/file"}) without(type) /
sum(vm_rows{type="indexdb/file"}) without(type)
sum(vm_data_size_bytes{type="indexdb/file"}) /
sum(vm_rows{type="indexdb/file"})
)
) < 3 * 24 * 3600 > 0
for: 30m

View File

@@ -0,0 +1,15 @@
groups:
- name: log-rules
type: vlogs
interval: 30s
rules:
- alert: AlwaysFiring
expr: '* | stats count()'
annotations:
description: "Generated more than {{$value}} log entries in the last 1 minute"
- alert: TooManyLogs
expr: '* | stats by (path) count() as total | filter total:>50'
annotations:
description: "Path {{$labels.path}} generated more than 50 log entries in the last 1 minute: {{$value}}"
- record: path:logs:count
expr: '* | stats by (path) count()'

View File

@@ -1,6 +1,6 @@
services:
vmagent:
image: victoriametrics/vmagent:v1.127.0
image: victoriametrics/vmagent:v1.125.1
depends_on:
- "victoriametrics"
ports:
@@ -14,7 +14,7 @@ services:
restart: always
victoriametrics:
image: victoriametrics/victoria-metrics:v1.127.0
image: victoriametrics/victoria-metrics:v1.125.1
ports:
- 8428:8428
volumes:
@@ -27,7 +27,7 @@ services:
restart: always
grafana:
image: grafana/grafana:12.2.0
image: grafana/grafana:12.1.1
depends_on:
- "victoriametrics"
ports:
@@ -40,7 +40,7 @@ services:
restart: always
vmalert:
image: victoriametrics/vmalert:v1.127.0
image: victoriametrics/vmalert:v1.125.1
depends_on:
- "victoriametrics"
ports:
@@ -59,7 +59,7 @@ services:
- '--external.alert.source=explore?orgId=1&left=["now-1h","now","VictoriaMetrics",{"expr": },{"mode":"Metrics"},{"ui":[true,true,true,"none"]}]'
restart: always
vmanomaly:
image: victoriametrics/vmanomaly:v1.26.2
image: victoriametrics/vmanomaly:v1.25.3
depends_on:
- "victoriametrics"
ports:
@@ -73,7 +73,7 @@ services:
- "/config.yaml"
- "--licenseFile=/license"
alertmanager:
image: prom/alertmanager:v0.28.1
image: prom/alertmanager:v0.28.0
volumes:
- ./alertmanager.yml:/config/alertmanager.yml
command:
@@ -83,7 +83,7 @@ services:
restart: always
node-exporter:
image: quay.io/prometheus/node-exporter:v1.9.1
image: quay.io/prometheus/node-exporter:v1.7.0
ports:
- 9100:9100
pid: host

View File

@@ -0,0 +1,17 @@
prepare-logs:
cd ./source_logs && bash download.sh
docker-up-elk:
docker-compose -f docker-compose.yml -f docker-compose-elk.yml up -d
docker-stop-elk:
docker-compose -f docker-compose.yml -f docker-compose-elk.yml stop
docker-up-loki:
docker-compose -f docker-compose.yml -f docker-compose-loki.yml up -d
docker-stop-loki:
docker-compose -f docker-compose.yml -f docker-compose-loki.yml stop
docker-cleanup:
docker-compose -f docker-compose.yml -f docker-compose-elk.yml -f docker-compose-loki.yml down -v --remove-orphans

View File

@@ -0,0 +1,69 @@
version: "3"
services:
filebeat-elastic:
image: docker.elastic.co/beats/filebeat:8.8.0
restart: on-failure
volumes:
- ./elk/filebeat/filebeat-elastic.yml:/usr/share/filebeat/filebeat.yml:ro
depends_on:
- elastic
filebeat-vlogs:
image: docker.elastic.co/beats/filebeat:8.8.0
restart: on-failure
volumes:
- ./elk/filebeat/filebeat-vlogs.yml:/usr/share/filebeat/filebeat.yml:ro
depends_on:
- vlogs
generator:
image: golang:1.25.0-alpine
restart: always
working_dir: /go/src/app
volumes:
- ./generator:/go/src/app
- ./source_logs:/go/src/source_logs
command:
- go
- run
- main.go
- -logsPath=/go/src/source_logs/logs
- -outputRateLimitItems=10000
- -syslog.addr=filebeat-elastic:12345
- -syslog.addr2=filebeat-vlogs:12345
- -logs.randomSuffix=false
depends_on: [filebeat-elastic, filebeat-vlogs]
elastic:
image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
volumes:
- ./elk/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
- elastic:/usr/share/elasticsearch/data
environment:
ES_JAVA_OPTS: "-Xmx2048m"
kibana:
image: docker.elastic.co/kibana/kibana:8.8.0
volumes:
- ./elk/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml
ports:
- "5601:5601"
depends_on: [elastic]
beat-exporter-elastic:
image: trustpilot/beat-exporter:0.4.0
command:
- -beat.uri=http://filebeat-elastic:5066
depends_on:
- filebeat-elastic
beat-exporter-vlogs:
image: trustpilot/beat-exporter:0.4.0
command:
- -beat.uri=http://filebeat-vlogs:5066
depends_on:
- filebeat-vlogs
volumes:
elastic:

View File

@@ -0,0 +1,51 @@
version: "3"
services:
generator:
image: golang:1.25.0-alpine
restart: always
working_dir: /go/src/app
volumes:
- ./generator:/go/src/app
- ./source_logs:/go/src/source_logs
command:
- go
- run
- main.go
- -logsPath=/go/src/source_logs/logs
- -outputRateLimitItems=10000
- -outputRateLimitPeriod=1s
- -syslog.addr=rsyslog:514
- -syslog.addr2=rsyslog:514
- -logs.randomSuffix=false
depends_on: [rsyslog]
loki:
image: grafana/loki:2.9.0
user: 0:0
ports:
- "3100:3100"
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- loki:/tmp/loki
- ./loki/:/etc/loki/
promtail:
image: grafana/promtail:2.9.0
command: -config.file=/etc/promtail/promtail-config.yaml
volumes:
- ./loki/:/etc/promtail/
depends_on:
- loki
- vlogs
rsyslog:
build:
dockerfile: Dockerfile
context: rsyslog
volumes:
- ./rsyslog/rsyslog.conf:/etc/rsyslog.conf
depends_on: [promtail]
volumes:
loki:

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