Compare commits

..

8 Commits

Author SHA1 Message Date
func25
f9e5881303 clean 2025-08-27 13:58:07 +07:00
func25
ab6fd0afed clean 2025-08-27 11:33:49 +07:00
func25
8f8ead2c50 clean 2025-08-27 10:47:30 +07:00
func25
2f422bad85 clean 2025-08-27 10:46:54 +07:00
func25
c0a41b41ca missing after cherry-pick 2025-08-27 10:27:05 +07:00
func25
68e493cef3 remove maxDebugSamples flag and limit checking 2025-08-27 10:12:17 +07:00
func25
06572772d4 update 2025-08-27 10:11:55 +07:00
func25
d12f6c280f update 2025-08-27 10:08:30 +07:00
230 changed files with 6632 additions and 6988 deletions

View File

@@ -1,37 +0,0 @@
name: check-commit-signed
on:
pull_request:
jobs:
check-commit-signed:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0 # we need full history for commit verification
- name: Check commit signatures
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "Not a PR event, skipping signature check"
exit 0
fi
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
echo "Checking commits in PR range: $RANGE"
if [ -z "$(git rev-list $RANGE)" ]; then
echo "No new commits in this PR, skipping signature check"
exit 0
fi
unsigned=$(git log --pretty="%H %G?" $RANGE | grep -vE " (G|E)$" || true)
if [ -n "$unsigned" ]; then
echo "Found unsigned commits:"
echo "$unsigned"
exit 1
fi
echo "All commits in PR are signed (G or E)"

View File

@@ -7,8 +7,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/firehose"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/opentelemetry/stream"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
@@ -68,7 +68,7 @@ func insertRows(at *auth.Token, tss []prompb.TimeSeries, mms []prompb.MetricMeta
ctx.WriteRequest.Timeseries = tssDst
var metadataTotal int
if prommetadata.IsEnabled() {
if promscrape.IsMetadataEnabled() {
var accountID, projectID uint32
if at != nil {
accountID = at.AccountID

View File

@@ -7,8 +7,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
@@ -36,7 +36,7 @@ func InsertHandler(at *auth.Token, req *http.Request) error {
return err
}
encoding := req.Header.Get("Content-Encoding")
return stream.Parse(req.Body, defaultTimestamp, encoding, true, prommetadata.IsEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, mms []prometheus.Metadata) error {
return insertRows(at, rows, mms, extraLabels)
}, func(s string) {
httpserver.LogError(req, s)

View File

@@ -6,8 +6,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/remotewrite"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/promremotewrite/stream"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/tenantmetrics"
@@ -71,7 +71,7 @@ func insertRows(at *auth.Token, timeseries []prompb.TimeSeries, mms []prompb.Met
ctx.WriteRequest.Timeseries = tssDst
var metadataTotal int
if prommetadata.IsEnabled() {
if promscrape.IsMetadataEnabled() {
var accountID, projectID uint32
if at != nil {
accountID = at.AccountID

View File

@@ -93,7 +93,10 @@ func TestParseRetryAfterHeader(t *testing.T) {
// helper calculate the max possible time duration calculated by timeutil.AddJitterToDuration.
func helper(d time.Duration) time.Duration {
dv := min(d/10, 10*time.Second)
dv := d / 10
if dv > 10*time.Second {
dv = 10 * time.Second
}
return d + dv
}

View File

@@ -34,7 +34,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutil"
"github.com/VictoriaMetrics/metrics"
)
@@ -85,8 +84,7 @@ func UnitTest(files []string, disableGroupLabel bool, externalLabels []string, e
defer server.Close()
} else {
httpListenAddr = httpListenPort
ln, err := net.Listen(netutil.GetTCPNetwork(), fmt.Sprintf(":%s", httpListenPort))
ln, err := net.Listen("tcp", fmt.Sprintf(":%s", httpListenPort))
if err != nil {
logger.Fatalf("cannot listen on port %s: %v", httpListenPort, err)
}

View File

@@ -132,7 +132,10 @@ func (ls Labels) String() string {
// a=[]Label{{Name: "a", Value: "2"}},b=[]Label{{Name: "a", Value: "1"}}, return 1
// a=[]Label{{Name: "a", Value: "1"}},b=[]Label{{Name: "a", Value: "1"}}, return 0
func LabelCompare(a, b Labels) int {
l := min(len(b), len(a))
l := len(a)
if len(b) < l {
l = len(b)
}
for i := 0; i < l; i++ {
if a[i].Name != b[i].Name {

View File

@@ -29,18 +29,6 @@ type manager struct {
groups map[uint64]*rule.Group
}
// groupAPI generates apiGroup object from group by its ID(hash)
func (m *manager) groupAPI(gID uint64) (*apiGroup, error) {
m.groupsMu.RLock()
defer m.groupsMu.RUnlock()
g, ok := m.groups[gID]
if !ok {
return nil, fmt.Errorf("can't find group with id %d", gID)
}
return groupToAPI(g), nil
}
// ruleAPI generates apiRule object from alert by its ID(hash)
func (m *manager) ruleAPI(gID, rID uint64) (apiRule, error) {
m.groupsMu.RLock()

View File

@@ -22,11 +22,10 @@ import (
// AlertManager represents integration provider with Prometheus alert manager
// https://github.com/prometheus/alertmanager
type AlertManager struct {
addr *url.URL
argFunc AlertURLGenerator
client *http.Client
timeout time.Duration
lastError string
addr *url.URL
argFunc AlertURLGenerator
client *http.Client
timeout time.Duration
authCfg *promauth.Config
// stores already parsed RelabelConfigs object
@@ -72,10 +71,6 @@ func (am AlertManager) Addr() string {
return am.addr.Redacted()
}
func (am *AlertManager) LastError() string {
return am.lastError
}
// Send an alert or resolve message
func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[string]string) error {
am.metrics.alertsSent.Add(len(alerts))
@@ -84,9 +79,6 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert, headers map[st
am.metrics.alertsSendDuration.UpdateDuration(startTime)
if err != nil {
am.metrics.alertsSendErrors.Add(len(alerts))
am.lastError = err.Error()
} else {
am.lastError = ""
}
return err
}

View File

@@ -18,11 +18,6 @@ type FakeNotifier struct {
// Close does nothing
func (*FakeNotifier) Close() {}
// LastError returns last error message
func (*FakeNotifier) LastError() string {
return ""
}
// Addr returns ""
func (*FakeNotifier) Addr() string { return "" }

View File

@@ -10,8 +10,6 @@ type Notifier interface {
Send(ctx context.Context, alerts []Alert, notifierHeaders map[string]string) error
// Addr returns address where alerts are sent.
Addr() string
// LastError returns error, that occured during last attempt to send data
LastError() string
// Close is a destructor for the Notifier
Close()
}

View File

@@ -25,11 +25,6 @@ func (bh *blackHoleNotifier) Close() {
bh.metrics.close()
}
// LastError return last notifier's error
func (bh *blackHoleNotifier) LastError() string {
return ""
}
// newBlackHoleNotifier creates a new blackHoleNotifier
func newBlackHoleNotifier() *blackHoleNotifier {
address := "blackhole"

View File

@@ -30,8 +30,6 @@ var (
{"api/v1/alerts", "list all active alerts"},
{"api/v1/notifiers", "list all notifiers"},
{fmt.Sprintf("api/v1/alert?%s=<int>&%s=<int>", paramGroupID, paramAlertID), "get alert status by group and alert ID"},
{fmt.Sprintf("api/v1/rule?%s=<int>&%s=<int>", paramGroupID, paramRuleID), "get rule status by group and rule ID"},
{fmt.Sprintf("api/v1/group?%s=<int>", paramGroupID), "get group status by group ID"},
}
systemLinks = [][2]string{
{"vmalert/groups", "UI"},
@@ -197,20 +195,6 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
w.Header().Set("Content-Type", "application/json")
w.Write(data)
return true
case "/vmalert/api/v1/group", "/api/v1/group":
group, err := rh.getGroup(r)
if err != nil {
httpserver.Errorf(w, r, "%s", err)
return true
}
data, err := json.Marshal(group)
if err != nil {
httpserver.Errorf(w, r, "failed to marshal group: %s", err)
return true
}
w.Header().Set("Content-Type", "application/json")
w.Write(data)
return true
case "/-/reload":
if !httpserver.CheckAuthFlag(w, r, reloadAuthKey) {
return true
@@ -225,18 +209,6 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
}
}
func (rh *requestHandler) getGroup(r *http.Request) (*apiGroup, error) {
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to read %q param: %w", paramGroupID, err)
}
obj, err := rh.m.groupAPI(groupID)
if err != nil {
return nil, errResponse(err, http.StatusNotFound)
}
return obj, nil
}
func (rh *requestHandler) getRule(r *http.Request) (apiRule, error) {
groupID, err := strconv.ParseUint(r.FormValue(paramGroupID), 10, 64)
if err != nil {
@@ -365,12 +337,12 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*apiGroup {
rule.Alerts = nil
}
if rule.LastError != "" {
g.unhealthy++
g.Unhealthy++
} else {
g.healthy++
g.Healthy++
}
if isNoMatch(rule) {
g.noMatch++
g.NoMatch++
}
filteredRules = append(filteredRules, rule)
}
@@ -487,9 +459,8 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
}
for _, target := range protoTargets {
notifier.Targets = append(notifier.Targets, &apiTarget{
Address: target.Addr(),
Labels: target.Labels.ToMap(),
LastError: target.LastError(),
Address: target.Addr(),
Labels: target.Labels.ToMap(),
})
}
lr.Data.Notifiers = append(lr.Data.Notifiers, notifier)

View File

@@ -113,7 +113,7 @@
{%= Controls(prefix, currentIcon, currentText, icons, filters, true) %}
{% if len(groups) > 0 %}
{% for _, g := range groups %}
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.unhealthy > 0 %} alert-danger{% endif %}">
<div id="group-{%s g.ID %}" class="d-flex w-100 border-0 flex-column group-items{% if g.Unhealthy > 0 %} alert-danger{% endif %}">
<span class="d-flex justify-content-between">
<a href="#group-{%s g.ID %}">{%s g.Name %}{% if g.Type != "prometheus" %} ({%s g.Type %}){% endif %} (every {%f.0 g.Interval %}s) #</a>
<span
@@ -123,9 +123,9 @@
data-bs-target="#sub-{%s g.ID %}"
>
<span class="d-flex gap-2">
{% if g.unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.unhealthy %}</span> {% endif %}
{% if g.noMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.noMatch %}</span> {% endif %}
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.healthy %}</span>
{% if g.Unhealthy > 0 %}<span class="badge bg-danger" title="Number of rules with status Error">{%d g.Unhealthy %}</span> {% endif %}
{% if g.NoMatch > 0 %}<span class="badge bg-warning" title="Number of rules with status NoMatch">{%d g.NoMatch %}</span> {% endif %}
<span class="badge bg-success" title="Number of rules with status Ok">{%d g.Healthy %}</span>
</span>
</span>
</span>

View File

@@ -363,7 +363,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
//line app/vmalert/web.qtpl:116
qw422016.N().S(`" class="d-flex w-100 border-0 flex-column group-items`)
//line app/vmalert/web.qtpl:116
if g.unhealthy > 0 {
if g.Unhealthy > 0 {
//line app/vmalert/web.qtpl:116
qw422016.N().S(` alert-danger`)
//line app/vmalert/web.qtpl:116
@@ -407,11 +407,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
<span class="d-flex gap-2">
`)
//line app/vmalert/web.qtpl:126
if g.unhealthy > 0 {
if g.Unhealthy > 0 {
//line app/vmalert/web.qtpl:126
qw422016.N().S(`<span class="badge bg-danger" title="Number of rules with status Error">`)
//line app/vmalert/web.qtpl:126
qw422016.N().D(g.unhealthy)
qw422016.N().D(g.Unhealthy)
//line app/vmalert/web.qtpl:126
qw422016.N().S(`</span> `)
//line app/vmalert/web.qtpl:126
@@ -420,11 +420,11 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
qw422016.N().S(`
`)
//line app/vmalert/web.qtpl:127
if g.noMatch > 0 {
if g.NoMatch > 0 {
//line app/vmalert/web.qtpl:127
qw422016.N().S(`<span class="badge bg-warning" title="Number of rules with status NoMatch">`)
//line app/vmalert/web.qtpl:127
qw422016.N().D(g.noMatch)
qw422016.N().D(g.NoMatch)
//line app/vmalert/web.qtpl:127
qw422016.N().S(`</span> `)
//line app/vmalert/web.qtpl:127
@@ -433,7 +433,7 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []*apiG
qw422016.N().S(`
<span class="badge bg-success" title="Number of rules with status Ok">`)
//line app/vmalert/web.qtpl:128
qw422016.N().D(g.healthy)
qw422016.N().D(g.Healthy)
//line app/vmalert/web.qtpl:128
qw422016.N().S(`</span>
</span>

View File

@@ -25,7 +25,6 @@ func TestHandler(t *testing.T) {
m := &manager{groups: map[uint64]*rule.Group{}}
var ar *rule.AlertingRule
var rr *rule.RecordingRule
var groupIDs []uint64
for _, dsType := range []string{"prometheus", "", "graphite"} {
g := rule.NewGroup(config.Group{
Name: "group",
@@ -46,9 +45,7 @@ func TestHandler(t *testing.T) {
ar = g.Rules[0].(*rule.AlertingRule)
rr = g.Rules[1].(*rule.RecordingRule)
g.ExecOnce(context.Background(), func() []notifier.Notifier { return nil }, nil, time.Time{})
id := g.CreateID()
m.groups[id] = g
groupIDs = append(groupIDs, id)
m.groups[g.CreateID()] = g
}
rh := &requestHandler{m: m}
@@ -191,21 +188,6 @@ func TestHandler(t *testing.T) {
t.Fatalf("expected %+v to have state updates field not empty", gotRuleWithUpdates.StateUpdates)
}
})
t.Run("/api/v1/group?groupID", func(t *testing.T) {
id := groupIDs[0]
g := m.groups[id]
expGroup := groupToAPI(g)
gotGroup := apiGroup{}
getResp(t, ts.URL+"/"+expGroup.APILink(), &gotGroup, 200)
if expGroup.ID != gotGroup.ID {
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
}
gotGroup = apiGroup{}
getResp(t, ts.URL+"/vmalert/"+expGroup.APILink(), &gotGroup, 200)
if expGroup.ID != gotGroup.ID {
t.Fatalf("expected to get Group %q; got %q instead", expGroup.ID, gotGroup.ID)
}
})
t.Run("/api/v1/rules&filters", func(t *testing.T) {
check := func(url string, statusCode, expGroups, expRules int) {

View File

@@ -28,8 +28,6 @@ type apiNotifier struct {
type apiTarget struct {
Address string `json:"address"`
Labels map[string]string `json:"labels"`
// LastError contains the error faced while sending to notifier.
LastError string `json:"lastError"`
}
// apiAlert represents a notifier.AlertingRule state
@@ -111,16 +109,11 @@ type apiGroup struct {
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
EvalDelay float64 `json:"eval_delay,omitempty"`
// Unhealthy unhealthy rules count
unhealthy int
Unhealthy int
// Healthy passing rules count
healthy int
Healthy int
// NoMatch not matching rules count
noMatch int
}
// APILink returns a link to the group's JSON representation.
func (ag *apiGroup) APILink() string {
return fmt.Sprintf("api/v1/group?%s=%s", paramGroupID, ag.ID)
NoMatch int
}
// groupAlerts represents a group of alerts for WEB view

View File

@@ -63,7 +63,10 @@ func (ts *TimeSeries) write(w io.Writer) (int, error) {
// Split long lines with more than 10K samples into multiple JSON lines.
// This should limit memory usage at VictoriaMetrics during data ingestion,
// since it allocates memory for the whole JSON line and processes it in one go.
batchSize := min(10000, len(timestamps))
batchSize := 10000
if batchSize > len(timestamps) {
batchSize = len(timestamps)
}
timestampsBatch := timestamps[:batchSize]
valuesBatch := values[:batchSize]
timestamps = timestamps[batchSize:]

View File

@@ -6,8 +6,8 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/common"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vminsert/relabel"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prommetadata"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/prometheus/stream"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/protoparser/protoparserutil"
@@ -30,7 +30,7 @@ func InsertHandler(req *http.Request) error {
return err
}
encoding := req.Header.Get("Content-Encoding")
return stream.Parse(req.Body, defaultTimestamp, encoding, true, prommetadata.IsEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
return stream.Parse(req.Body, defaultTimestamp, encoding, true, promscrape.IsMetadataEnabled(), func(rows []prometheus.Row, _ []prometheus.Metadata) error {
return insertRows(rows, extraLabels)
}, func(s string) {
httpserver.LogError(req, s)

View File

@@ -142,12 +142,6 @@ func (s *series) summarize(aggrFunc aggrFunc, startTime, endTime, step int64, xF
}
func execExpr(ec *evalConfig, query string) (nextSeriesFunc, error) {
// Validate query length to prevent memory exhaustion
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return nil, fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
}
expr, err := graphiteql.Parse(query)
if err != nil {
return nil, fmt.Errorf("cannot parse %q: %w", query, err)

View File

@@ -4070,9 +4070,6 @@ func TestExecExprFailure(t *testing.T) {
f(`holtWintersConfidenceArea(group(time("foo.baz",15),time("foo.baz",15)))`)
f(`holtWintersConfidenceArea()`)
// too long query
f(`sumSeries(` + strings.Repeat("metric.very.long.name.that.takes.space,", 500) + `metric.final)`)
}
func compareSeries(ss, ssExpected []*series, expr graphiteql.Expr) error {

View File

@@ -1218,7 +1218,10 @@ func transformDelay(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, er
values := s.Values
stepsLocal := steps
if stepsLocal < 0 {
stepsLocal = min(-stepsLocal, len(values))
stepsLocal = -stepsLocal
if stepsLocal > len(values) {
stepsLocal = len(values)
}
copy(values, values[stepsLocal:])
for i := len(values) - 1; i >= len(values)-stepsLocal; i-- {
values[i] = nan
@@ -4660,14 +4663,20 @@ func transformSubstr(ec *evalConfig, fe *graphiteql.FuncExpr) (nextSeriesFunc, e
if start > len(splitName) {
start = len(splitName)
} else if start < 0 {
start = max(len(splitName)+start, 0)
start = len(splitName) + start
if start < 0 {
start = 0
}
}
if stop == 0 {
stop = len(splitName)
} else if stop > len(splitName) {
stop = len(splitName)
} else if stop < 0 {
stop = max(len(splitName)+stop, 0)
stop = len(splitName) + stop
if stop < 0 {
stop = 0
}
}
if stop < start {
stop = start

View File

@@ -2,7 +2,6 @@ package vmselect
import (
"embed"
"encoding/json"
"flag"
"fmt"
"net/http"
@@ -49,10 +48,13 @@ var (
var slowQueries = metrics.NewCounter(`vm_slow_queries_total`)
func getDefaultMaxConcurrentRequests() int {
// A single request can saturate all the CPU cores, so there is no sense
// in allowing higher number of concurrent requests - they will just contend
// for unavailable CPU time.
n := min(cgroup.AvailableCPUs()*2, 16)
n := cgroup.AvailableCPUs() * 2
if n > 16 {
// A single request can saturate all the CPU cores, so there is no sense
// in allowing higher number of concurrent requests - they will just contend
// for unavailable CPU time.
n = 16
}
return n
}
@@ -65,7 +67,6 @@ func Init() {
prometheus.InitMaxUniqueTimeseries(*maxConcurrentRequests)
concurrencyLimitCh = make(chan struct{}, *maxConcurrentRequests)
initVMUIConfig()
initVMAlertProxy()
}
@@ -127,7 +128,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
default:
// Sleep for a while until giving up. This should resolve short bursts in requests.
concurrencyLimitReached.Inc()
d := min(searchutil.GetMaxQueryDuration(r), *maxQueueDuration)
d := searchutil.GetMaxQueryDuration(r)
if d > *maxQueueDuration {
d = *maxQueueDuration
}
t := timerpool.Get(d)
select {
case concurrencyLimitCh <- struct{}{}:
@@ -258,6 +262,13 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true
}
return true
case "/api/v1/config":
httpserver.EnableCORS(w, r)
if err := prometheus.ConfigHandler(qt, startTime, w, r); err != nil {
httpserver.SendPrometheusError(w, r, err)
return true
}
return true
case "/api/v1/export":
exportRequests.Inc()
if err := prometheus.ExportHandler(startTime, w, r); err != nil {
@@ -456,11 +467,6 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
return true
}
if strings.HasPrefix(path, "/vmui/") {
if path == "/vmui/config.json" {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, vmuiConfig)
return true
}
if strings.HasPrefix(path, "/vmui/static/") {
// Allow clients caching static contents for long period of time, since it shouldn't change over time.
// Path to static contents (such as js and css) must be changed whenever its contents is changed.
@@ -539,6 +545,13 @@ func handleStaticAndSimpleRequests(w http.ResponseWriter, r *http.Request, path
expandWithExprsRequests.Inc()
prometheus.ExpandWithExprs(w, r)
return true
case "/extract-metric-exprs":
startTime := time.Now()
if err := prometheus.ExtractMetricExprsHandler(startTime, w, r); err != nil {
httpserver.Errorf(w, r, "%s", err)
return true
}
return true
case "/prettify-query":
prettifyQueryRequests.Inc()
prometheus.PrettifyQuery(w, r)
@@ -735,34 +748,8 @@ func proxyVMAlertRequests(w http.ResponseWriter, r *http.Request) {
var (
vmalertProxyHost string
vmalertProxy *nethttputil.ReverseProxy
vmuiConfig string
)
func initVMUIConfig() {
var cfg struct {
License struct {
Type string `json:"type"`
} `json:"license"`
VMAlert struct {
Enabled bool `json:"enabled"`
} `json:"vmalert"`
}
data, err := vmuiFiles.ReadFile("vmui/config.json")
if err != nil {
logger.Fatalf("cannot read vmui default config: %s", err)
}
err = json.Unmarshal(data, &cfg)
if err != nil {
logger.Fatalf("cannot parse vmui default config: %s", err)
}
cfg.VMAlert.Enabled = len(*vmalertProxyURL) != 0
data, err = json.Marshal(&cfg)
if err != nil {
logger.Fatalf("cannot create vmui config: %s", err)
}
vmuiConfig = string(data)
}
// initVMAlertProxy must be called after flag.Parse(), since it uses command-line flags.
func initVMAlertProxy() {
if len(*vmalertProxyURL) == 0 {

View File

@@ -63,10 +63,18 @@ type Results struct {
packedTimeseries []packedTimeseries
sr *storage.Search
tbf *tmpBlocksFile
// the result is simulated
isSimulated bool
simulatedSeries []*storage.SimulatedSamples
}
// Len returns the number of results in rss.
func (rss *Results) Len() int {
if rss.isSimulated {
return len(rss.simulatedSeries)
}
return len(rss.packedTimeseries)
}
@@ -203,7 +211,10 @@ var defaultMaxWorkersPerQuery = func() int {
// for processing an average query, without significant impact on inter-CPU communications.
const maxWorkersLimit = 32
n := min(gomaxprocs, maxWorkersLimit)
n := gomaxprocs
if n > maxWorkersLimit {
n = maxWorkersLimit
}
return n
}()
@@ -215,6 +226,10 @@ var defaultMaxWorkersPerQuery = func() int {
//
// rss becomes unusable after the call to RunParallel.
func (rss *Results) RunParallel(qt *querytracer.Tracer, f func(rs *Result, workerID uint) error) error {
if rss.isSimulated {
return rss.runParallelSimulated(qt, f)
}
qt = qt.NewChild("parallel process of fetched data")
defer rss.mustClose()
@@ -230,6 +245,87 @@ func (rss *Results) RunParallel(qt *querytracer.Tracer, f func(rs *Result, worke
return err
}
func (rss *Results) runParallelSimulated(qt *querytracer.Tracer, f func(rs *Result, workerID uint) error) error {
qt = qt.NewChild("parallel process of fetched data")
cb := f
tmpResult := getTmpResult()
defer putTmpResult(tmpResult)
// For simplicity, let's process serially first. Parallelization can be added if needed.
// If parallelization is desired, it would mirror the worker pool logic of the original runParallel,
// but iterating over rss.simulatedSamples entries.
workerID := uint(0)
var firstErr error
for _, metric := range rss.simulatedSeries {
r := &tmpResult.rs
r.reset()
r.MetricName.CopyFrom(&metric.Name)
for i, ts := range metric.Timestamps {
if ts >= rss.tr.MinTimestamp && ts <= rss.tr.MaxTimestamp {
r.Values = append(r.Values, metric.Value[i])
r.Timestamps = append(r.Timestamps, ts)
}
}
// Sort timestamps chronologically to match real storage behavior.
// Real storage ensures chronological order through:
// 1. Block-level sorting by MinTimestamp
// 2. Within-block timestamp ordering via encoding.EnsureNonDecreasingSequence()
if len(r.Timestamps) > 1 {
// Create pairs for sorting
type timestampValue struct {
timestamp int64
value float64
}
pairs := make([]timestampValue, len(r.Timestamps))
for i := range r.Timestamps {
pairs[i] = timestampValue{
timestamp: r.Timestamps[i],
value: r.Values[i],
}
}
// Sort by timestamp
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].timestamp < pairs[j].timestamp
})
// Extract back to separate slices
for i := range pairs {
r.Timestamps[i] = pairs[i].timestamp
r.Values[i] = pairs[i].value
}
}
// The input from the client is most likely already deduplicated, since it's emitted by
// vmselect. However, the client may modify the input instead of using the returned one.
dedupInterval := storage.GetDedupInterval()
if dedupInterval > 0 && len(r.Timestamps) > 0 {
r.Timestamps, r.Values = storage.DeduplicateSamples(r.Timestamps, r.Values, dedupInterval)
}
rowProcessed := len(r.Timestamps)
if rowProcessed > 0 {
err := cb(r, workerID)
if err != nil {
firstErr = err
break
}
}
}
// Count total samples across all series
totalSamples := 0
for _, metric := range rss.simulatedSeries {
totalSamples += len(metric.Timestamps)
}
qt.Donef("series=%d, samples=%d", len(rss.simulatedSeries), totalSamples)
return firstErr
}
func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, workerID uint) error) (int, error) {
tswsLen := len(rss.packedTimeseries)
if tswsLen == 0 {
@@ -276,7 +372,10 @@ func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, worke
}
// Prepare worker channels.
workers := min(len(tsws), maxWorkers)
workers := len(tsws)
if workers > maxWorkers {
workers = maxWorkers
}
itemsPerWorker := (len(tsws) + workers - 1) / workers
workChs := make([]chan *timeseriesWork, workers)
for i := range workChs {
@@ -491,7 +590,10 @@ func (pts *packedTimeseries) unpackTo(dst []*sortBlock, tbf *tmpBlocksFile, tr s
}
// Prepare worker channels.
workers := min(len(upws), gomaxprocs)
workers := len(upws)
if workers > gomaxprocs {
workers = gomaxprocs
}
if workers < 1 {
workers = 1
}
@@ -1110,6 +1212,10 @@ func SearchMetricNames(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline
//
// Results.RunParallel or Results.Cancel must be called on the returned Results.
func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutil.Deadline) (*Results, error) {
if len(sq.SimulatedSeries) > 0 {
return processSearchSimulated(qt, sq, deadline)
}
qt = qt.NewChild("fetch matching series: %s", sq)
defer qt.Done()
if deadline.Exceeded() {
@@ -1144,7 +1250,10 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
// metricNamesBuf is used for holding all the loaded unique metric names at m and orderedMetricNames.
// It should reduce pressure on Go GC by reducing the number of string allocations
// when constructing metricName string from byte slice.
metricNamesBufCap := min(maxSeriesCount*100, maxFastAllocBlockSize)
metricNamesBufCap := maxSeriesCount * 100
if metricNamesBufCap > maxFastAllocBlockSize {
metricNamesBufCap = maxFastAllocBlockSize
}
metricNamesBuf := make([]byte, 0, metricNamesBufCap)
// brssPool is used for holding all the blockRefs objects across all the loaded time series.
@@ -1153,7 +1262,10 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
// brsPool is used for holding the most of blockRefs.brs slices across all the loaded time series.
// It should reduce pressure on Go GC by reducing the number of allocations for blockRefs.brs slices.
brsPoolCap := min(uintptr(maxSeriesCount), maxFastAllocBlockSize/unsafe.Sizeof(blockRef{}))
brsPoolCap := uintptr(maxSeriesCount)
if brsPoolCap > maxFastAllocBlockSize/unsafe.Sizeof(blockRef{}) {
brsPoolCap = maxFastAllocBlockSize / unsafe.Sizeof(blockRef{})
}
brsPool := make([]blockRef, 0, brsPoolCap)
// m maps from metricName to the index of blockRefs inside brssPool
@@ -1276,6 +1388,41 @@ func ProcessSearchQuery(qt *querytracer.Tracer, sq *storage.SearchQuery, deadlin
return &rss, nil
}
func processSearchSimulated(qt *querytracer.Tracer, sq *storage.SearchQuery, deadline searchutil.Deadline) (*Results, error) {
qt = qt.NewChild("fetch matching series (simulated): %s", sq)
defer qt.Done()
if deadline.Exceeded() {
return nil, fmt.Errorf("timeout exceeded before starting the query processing: %s", deadline.String())
}
tr := storage.TimeRange{
MinTimestamp: sq.MinTimestamp,
MaxTimestamp: sq.MaxTimestamp,
}
// Process simulated samples.
matchedSamples, err := storage.MatchSimulatedSamples(sq.SimulatedSeries, sq.TagFilterss)
if err != nil {
return nil, fmt.Errorf("cannot match simulated samples: %w", err)
}
// Create a result set similar to ProcessSearchQuery
rss := &Results{
tr: tr,
deadline: deadline,
isSimulated: true,
simulatedSeries: matchedSamples,
}
if len(matchedSamples) == 0 {
qt.Printf("no matching series found")
} else {
qt.Printf("found %d series", len(rss.simulatedSeries))
}
return rss, nil
}
type blockRef struct {
partRef storage.PartRef
addr tmpBlockAddr

View File

@@ -0,0 +1,20 @@
{% import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
) %}
{% stripspace %}
ConfigResponse generates response for /api/v1/config .
{% func ConfigResponse(config *ConfigData, qt *querytracer.Tracer) %}
{
"status":"success",
"data":{
"minStalenessInterval": {%q= config.MinStalenessInterval %},
"maxStalenessInterval": {%q= config.MaxStalenessInterval %}
}
{% code qt.Done() %}
{%= dumpQueryTrace(qt) %}
}
{% endfunc %}
{% endstripspace %}

View File

@@ -0,0 +1,73 @@
// Code generated by qtc from "config_response.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line app/vmselect/prometheus/config_response.qtpl:1
package prometheus
//line app/vmselect/prometheus/config_response.qtpl:1
import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
)
// ConfigResponse generates response for /api/v1/config .
//line app/vmselect/prometheus/config_response.qtpl:8
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line app/vmselect/prometheus/config_response.qtpl:8
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line app/vmselect/prometheus/config_response.qtpl:8
func StreamConfigResponse(qw422016 *qt422016.Writer, config *ConfigData, qt *querytracer.Tracer) {
//line app/vmselect/prometheus/config_response.qtpl:8
qw422016.N().S(`{"status":"success","data":{"minStalenessInterval":`)
//line app/vmselect/prometheus/config_response.qtpl:12
qw422016.N().Q(config.MinStalenessInterval)
//line app/vmselect/prometheus/config_response.qtpl:12
qw422016.N().S(`,"maxStalenessInterval":`)
//line app/vmselect/prometheus/config_response.qtpl:13
qw422016.N().Q(config.MaxStalenessInterval)
//line app/vmselect/prometheus/config_response.qtpl:13
qw422016.N().S(`}`)
//line app/vmselect/prometheus/config_response.qtpl:15
qt.Done()
//line app/vmselect/prometheus/config_response.qtpl:16
streamdumpQueryTrace(qw422016, qt)
//line app/vmselect/prometheus/config_response.qtpl:16
qw422016.N().S(`}`)
//line app/vmselect/prometheus/config_response.qtpl:18
}
//line app/vmselect/prometheus/config_response.qtpl:18
func WriteConfigResponse(qq422016 qtio422016.Writer, config *ConfigData, qt *querytracer.Tracer) {
//line app/vmselect/prometheus/config_response.qtpl:18
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/config_response.qtpl:18
StreamConfigResponse(qw422016, config, qt)
//line app/vmselect/prometheus/config_response.qtpl:18
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/config_response.qtpl:18
}
//line app/vmselect/prometheus/config_response.qtpl:18
func ConfigResponse(config *ConfigData, qt *querytracer.Tracer) string {
//line app/vmselect/prometheus/config_response.qtpl:18
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/config_response.qtpl:18
WriteConfigResponse(qb422016, config, qt)
//line app/vmselect/prometheus/config_response.qtpl:18
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/config_response.qtpl:18
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/config_response.qtpl:18
return qs422016
//line app/vmselect/prometheus/config_response.qtpl:18
}

View File

@@ -0,0 +1,18 @@
{% stripspace %}
ExtractMetricExprsResponse generates response for /extract-metric-exprs .
{% func ExtractMetricExprsResponse(metrics []string) %}
{
"status":"success",
"data":[
{% if len(metrics) > 0 %}
{%q= metrics[0] %}
{% for i := 1; i < len(metrics); i++ %}
,{%q= metrics[i] %}
{% endfor %}
{% endif %}
]
}
{% endfunc %}
{% endstripspace %}

View File

@@ -0,0 +1,69 @@
// Code generated by qtc from "extract_metric_exprs_response.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
// ExtractMetricExprsResponse generates response for /extract-metric-exprs .
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:4
package prometheus
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:4
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:4
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:4
func StreamExtractMetricExprsResponse(qw422016 *qt422016.Writer, metrics []string) {
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:4
qw422016.N().S(`{"status":"success","data":[`)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:8
if len(metrics) > 0 {
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:9
qw422016.N().Q(metrics[0])
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:10
for i := 1; i < len(metrics); i++ {
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:10
qw422016.N().S(`,`)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:11
qw422016.N().Q(metrics[i])
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:12
}
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:13
}
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:13
qw422016.N().S(`]}`)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
}
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
func WriteExtractMetricExprsResponse(qq422016 qtio422016.Writer, metrics []string) {
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
StreamExtractMetricExprsResponse(qw422016, metrics)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
}
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
func ExtractMetricExprsResponse(metrics []string) string {
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
WriteExtractMetricExprsResponse(qb422016, metrics)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
return qs422016
//line app/vmselect/prometheus/extract_metric_exprs_response.qtpl:16
}

View File

@@ -1,8 +1,10 @@
package prometheus
import (
"encoding/json"
"flag"
"fmt"
"io"
"math"
"net/http"
"runtime"
@@ -20,10 +22,12 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/promql"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/querystats"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bufferedwriter"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
@@ -37,9 +41,13 @@ var (
latencyOffset = flag.Duration("search.latencyOffset", time.Second*30, "The time when data points become visible in query results after the collection. "+
"It can be overridden on per-query basis via latency_offset arg. "+
"Too small value can result in incomplete last points for query results")
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
maxLookback = flag.Duration("search.maxLookback", 0, "Synonym to -query.lookback-delta from Prometheus. "+
"The value is dynamically detected from interval between time series datapoints if not set. It can be overridden on per-query basis via max_lookback arg. "+
"See also '-search.maxStalenessInterval' flag, which has the same meaning due to historical reasons")
minStalenessInterval = flag.Duration("search.minStalenessInterval", 0, "The minimum interval for staleness calculations. "+
"This flag could be useful for removing gaps on graphs generated from time series with irregular intervals between samples. "+
"See also '-search.maxStalenessInterval'")
maxStalenessInterval = flag.Duration("search.maxStalenessInterval", 0, "The maximum interval for staleness calculations. "+
"By default, it is automatically calculated from the median interval between samples. This flag could be useful for tuning "+
"Prometheus data model closer to Influx-style data model. See https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness for details. "+
@@ -114,7 +122,7 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
if err != nil {
return err
}
lookbackDelta, err := getMaxLookback(r)
lookbackDelta, err := getMaxLookback(r, *maxStalenessInterval)
if err != nil {
return err
}
@@ -609,6 +617,55 @@ func TSDBStatusHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
var tsdbStatusDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/status/tsdb"}`)
// ConfigData holds the current configuration values for search-related flags
type ConfigData struct {
MinStalenessInterval string
MaxStalenessInterval string
}
// ConfigHandler processes /api/v1/config request.
//
// It returns the current configuration for search-related flags.
func ConfigHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWriter, _ *http.Request) error {
config := &ConfigData{
MinStalenessInterval: (*minStalenessInterval).String(),
MaxStalenessInterval: (*maxStalenessInterval).String(),
}
w.Header().Set("Content-Type", "application/json")
bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw)
WriteConfigResponse(bw, config, qt)
if err := bw.Flush(); err != nil {
return fmt.Errorf("cannot send config response to remote client: %w", err)
}
return nil
}
// ExtractMetricExprsHandler processes /extract-metric-exprs request.
//
// It extracts metric expressions from a given PromQL query.
func ExtractMetricExprsHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
query := r.FormValue("query")
if len(query) == 0 {
return fmt.Errorf("missing `query` arg")
}
metrics, err := promql.ExtractMetricsFromQuery(query)
if err != nil {
return fmt.Errorf("cannot extract metrics from query: %w", err)
}
w.Header().Set("Content-Type", "application/json")
bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw)
WriteExtractMetricExprsResponse(bw, metrics)
if err := bw.Flush(); err != nil {
return fmt.Errorf("cannot send extract metric exprs response to remote client: %w", err)
}
return nil
}
// LabelsHandler processes /api/v1/labels request.
//
// See https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names
@@ -710,7 +767,8 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
ct := startTime.UnixNano() / 1e6
deadline := searchutil.GetDeadlineForQuery(r, startTime)
mayCache := !httputil.GetBool(r, "nocache")
isDebug := httputil.GetBool(r, "debug")
noCache := httputil.GetBool(r, "nocache") || isDebug
query := r.FormValue("query")
if len(query) == 0 {
return fmt.Errorf("missing `query` arg")
@@ -719,7 +777,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
if err != nil {
return err
}
lookbackDelta, err := getMaxLookback(r)
lookbackDelta, err := getMaxLookback(r, *maxStalenessInterval)
if err != nil {
return err
}
@@ -731,9 +789,8 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
step = defaultStep
}
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
if len(query) > maxQueryLen.IntN() {
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
}
etfs, err := searchutil.GetExtraTagFilters(r)
if err != nil {
@@ -806,23 +863,14 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
} else {
queryOffset = 0
}
ec := &promql.EvalConfig{
Start: start,
End: start,
Step: step,
MaxPointsPerSeries: *maxPointsPerTimeseries,
MaxSeries: GetMaxUniqueTimeSeries(),
QuotedRemoteAddr: httpserver.GetQuotedRemoteAddr(r),
Deadline: deadline,
MayCache: mayCache,
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},
ec := newEvalConfig(r, start, start, step, deadline, noCache, lookbackDelta, isDebug, etfs)
if isDebug {
if err := populateSimulatedData(r, nil, ec); err != nil {
_ = r.Body.Close()
return fmt.Errorf("cannot read simulated samples: %w", err)
}
}
_ = r.Body.Close()
qs := promql.NewQueryStats(query, nil, ec)
ec.QueryStats = qs
@@ -896,16 +944,16 @@ func QueryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWriter, query string,
start, end, step int64, r *http.Request, ct int64, etfs [][]storage.TagFilter) error {
deadline := searchutil.GetDeadlineForQuery(r, startTime)
mayCache := !httputil.GetBool(r, "nocache")
lookbackDelta, err := getMaxLookback(r)
isDebug := httputil.GetBool(r, "debug")
noCache := httputil.GetBool(r, "nocache") || isDebug
lookbackDelta, err := getMaxLookback(r, *maxStalenessInterval)
if err != nil {
return err
}
// Validate input args.
maxLen := searchutil.GetMaxQueryLen()
if len(query) > maxLen {
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxLen)
if len(query) > maxQueryLen.IntN() {
return fmt.Errorf("too long query; got %d bytes; mustn't exceed `-search.maxQueryLen=%d` bytes", len(query), maxQueryLen.N)
}
if start > end {
end = start + defaultStep
@@ -913,27 +961,19 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
if err := promql.ValidateMaxPointsPerSeries(start, end, step, *maxPointsPerTimeseries); err != nil {
return fmt.Errorf("%w; (see -search.maxPointsPerTimeseries command-line flag)", err)
}
if mayCache {
if !noCache {
start, end = promql.AdjustStartEnd(start, end, step)
}
ec := &promql.EvalConfig{
Start: start,
End: end,
Step: step,
MaxPointsPerSeries: *maxPointsPerTimeseries,
MaxSeries: GetMaxUniqueTimeSeries(),
QuotedRemoteAddr: httpserver.GetQuotedRemoteAddr(r),
Deadline: deadline,
MayCache: mayCache,
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},
ec := newEvalConfig(r, start, end, step, deadline, noCache, lookbackDelta, isDebug, etfs)
if isDebug {
if err := populateSimulatedData(r, nil, ec); err != nil {
_ = r.Body.Close()
return fmt.Errorf("cannot read simulated samples: %w", err)
}
}
_ = r.Body.Close()
qs := promql.NewQueryStats(query, nil, ec)
ec.QueryStats = qs
@@ -969,6 +1009,93 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
return nil
}
func newEvalConfig(r *http.Request, start, end, step int64, deadline searchutil.Deadline, noCache bool, lookbackDelta int64, isDebug bool, etfs [][]storage.TagFilter) *promql.EvalConfig {
ec := &promql.EvalConfig{
Start: start,
End: end,
Step: step,
MaxPointsPerSeries: *maxPointsPerTimeseries,
MaxSeries: GetMaxUniqueTimeSeries(),
MinStalenessInterval: *minStalenessInterval,
QuotedRemoteAddr: httpserver.GetQuotedRemoteAddr(r),
Deadline: deadline,
MayCache: !noCache,
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},
}
return ec
}
func populateSimulatedData(r *http.Request, at *auth.Token, evalConfig *promql.EvalConfig) error {
type jsonExportBlockInput struct {
Metric map[string]string `json:"metric"`
Values []float64 `json:"values"`
Timestamps []int64 `json:"timestamps"`
}
// --- Read and Parse Input Samples from r.Body ---
var simulatedSeries []*storage.SimulatedSamples
decoder := json.NewDecoder(r.Body)
lineNum := 0
for {
var jeb jsonExportBlockInput
if err := decoder.Decode(&jeb); err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("error decoding input JSON on line %d: %w", lineNum, err)
}
// Validate that values and timestamps arrays have the same length
if len(jeb.Values) != len(jeb.Timestamps) {
return fmt.Errorf("mismatched values and timestamps arrays length in debug data on line %d: values=%d, timestamps=%d", lineNum, len(jeb.Values), len(jeb.Timestamps))
}
var mn = storage.GetMetricName()
defer storage.PutMetricName(mn)
for k, v := range jeb.Metric {
mn.AddTag(k, v)
}
ss := &storage.SimulatedSamples{
Value: jeb.Values,
Timestamps: jeb.Timestamps,
}
ss.Name.CopyFrom(mn)
simulatedSeries = append(simulatedSeries, ss)
lineNum++
}
// It doesn't make sense to debug with empty samples
if len(simulatedSeries) == 0 {
return fmt.Errorf("no simulated samples found")
}
minStalenessInterval, err := httputil.GetDurationRaw(r, "min_staleness_interval", evalConfig.MinStalenessInterval)
if err != nil {
return fmt.Errorf("cannot parse `min_staleness_interval` arg: %w", err)
}
maxStalenessInterval, err := httputil.GetDurationRaw(r, "max_staleness_interval", *maxStalenessInterval)
if err != nil {
return fmt.Errorf("cannot parse `max_staleness_interval` arg: %w", err)
}
evalConfig.SimulatedSamples = simulatedSeries
evalConfig.MinStalenessInterval = minStalenessInterval
evalConfig.LookbackDelta, err = getMaxLookback(r, maxStalenessInterval)
if err != nil {
return err
}
return nil
}
func removeEmptyValuesAndTimeseries(tss []netstorage.Result) []netstorage.Result {
dst := tss[:0]
for i := range tss {
@@ -1044,7 +1171,7 @@ func adjustLastPoints(tss []netstorage.Result, start, end int64) []netstorage.Re
return tss
}
func getMaxLookback(r *http.Request) (int64, error) {
func getMaxLookback(r *http.Request, maxStalenessInterval time.Duration) (int64, error) {
d := maxLookback.Milliseconds()
if d == 0 {
d = maxStalenessInterval.Milliseconds()
@@ -1089,9 +1216,12 @@ func getRoundDigits(r *http.Request) int {
}
func getLatencyOffsetMilliseconds(r *http.Request) (int64, error) {
// Zero latency offset may be useful for some use cases.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
d := max(latencyOffset.Milliseconds(), 0)
d := latencyOffset.Milliseconds()
if d < 0 {
// Zero latency offset may be useful for some use cases.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2061#issuecomment-1299109836
d = 0
}
return httputil.GetDuration(r, "latency_offset", d)
}

View File

@@ -161,8 +161,11 @@ func aggrFuncAny(afa *aggrFuncArg) ([]*timeseries, error) {
afe := func(tss []*timeseries, _ *metricsql.ModifierExpr) []*timeseries {
return tss[:1]
}
// Only a single time series per group must be returned
limit := min(afa.ae.Limit, 1)
limit := afa.ae.Limit
if limit > 1 {
// Only a single time series per group must be returned
limit = 1
}
return aggrFuncExt(afe, tss, &afa.ae.Modifier, limit, true)
}

View File

@@ -134,6 +134,10 @@ type EvalConfig struct {
// LookbackDelta is analog to `-query.lookback-delta` from Prometheus.
LookbackDelta int64
// MaxStalenessInterval corresponds to -search.maxStalenessInterval,
// but customized per query request.
MinStalenessInterval time.Duration
// How many decimal digits after the point to leave in response.
RoundDigits int
@@ -158,6 +162,9 @@ type EvalConfig struct {
timestamps []int64
timestampsOnce sync.Once
// Simulated samples
SimulatedSamples []*storage.SimulatedSamples
}
// copyEvalConfig returns src copy.
@@ -176,6 +183,8 @@ func copyEvalConfig(src *EvalConfig) *EvalConfig {
ec.CacheTagFilters = src.CacheTagFilters
ec.GetRequestURI = src.GetRequestURI
ec.QueryStats = src.QueryStats
ec.MinStalenessInterval = src.MinStalenessInterval
ec.SimulatedSamples = src.SimulatedSamples
// do not copy src.timestamps - they must be generated again.
return &ec
@@ -929,7 +938,7 @@ func evalRollupFuncWithSubquery(qt *querytracer.Tracer, ec *EvalConfig, funcName
}
ecSQ := copyEvalConfig(ec)
ecSQ.Start -= window + step + maxSilenceInterval()
ecSQ.Start -= window + step + maxSilenceInterval(ec.MinStalenessInterval)
ecSQ.End += step
ecSQ.Step = step
ecSQ.MaxPointsPerSeries = *maxPointsSubqueryPerTimeseries
@@ -946,7 +955,7 @@ func evalRollupFuncWithSubquery(qt *querytracer.Tracer, ec *EvalConfig, funcName
return nil, nil
}
sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps)
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps, ec.MinStalenessInterval)
if err != nil {
return nil, err
}
@@ -1002,7 +1011,10 @@ func getKeepMetricNames(expr metricsql.Expr) bool {
}
func doParallel(tss []*timeseries, f func(ts *timeseries, values []float64, timestamps []int64, workerID uint) ([]float64, []int64)) {
workers := min(netstorage.MaxWorkers(), len(tss))
workers := netstorage.MaxWorkers()
if workers > len(tss) {
workers = len(tss)
}
seriesPerWorker := (len(tss) + workers - 1) / workers
workChs := make([]chan *timeseries, workers)
for i := range workChs {
@@ -1076,7 +1088,10 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
return evalRollupFuncNoCache(qt, ecCopy, funcName, rf, expr, me, iafc, window, pointsPerSeries)
}
tooBigOffset := func(offset int64) bool {
maxOffset := min(window/2, 1800*1000)
maxOffset := window / 2
if maxOffset > 1800*1000 {
maxOffset = 1800 * 1000
}
return offset >= maxOffset
}
deleteCachedSeries := func(qt *querytracer.Tracer) {
@@ -1678,7 +1693,7 @@ func evalRollupFuncNoCache(qt *querytracer.Tracer, ec *EvalConfig, funcName stri
}
// Obtain rollup configs before fetching data from db, so type errors could be caught earlier.
sharedTimestamps := getTimestamps(ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries)
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps)
preFunc, rcs, err := getRollupConfigs(funcName, rf, expr, ec.Start, ec.End, ec.Step, ec.MaxPointsPerSeries, window, ec.LookbackDelta, sharedTimestamps, ec.MinStalenessInterval)
if err != nil {
return nil, err
}
@@ -1688,7 +1703,7 @@ func evalRollupFuncNoCache(qt *querytracer.Tracer, ec *EvalConfig, funcName stri
tfss = searchutil.JoinTagFilterss(tfss, ec.EnforcedTagFilterss)
minTimestamp := ec.Start
if needSilenceIntervalForRollupFunc[funcName] {
minTimestamp -= maxSilenceInterval()
minTimestamp -= maxSilenceInterval(ec.MinStalenessInterval)
}
if window > ec.Step {
minTimestamp -= window
@@ -1696,6 +1711,8 @@ func evalRollupFuncNoCache(qt *querytracer.Tracer, ec *EvalConfig, funcName stri
minTimestamp -= ec.Step
}
sq := storage.NewSearchQuery(minTimestamp, ec.End, tfss, ec.MaxSeries)
sq.SimulatedSeries = ec.SimulatedSamples
rss, err := netstorage.ProcessSearchQuery(qt, sq, ec.Deadline)
if err != nil {
return nil, err
@@ -1781,7 +1798,7 @@ func getRollupMemoryLimiter() *memoryLimiter {
return &rollupMemoryLimiter
}
func maxSilenceInterval() int64 {
func maxSilenceInterval(minStalenessInterval time.Duration) int64 {
d := minStalenessInterval.Milliseconds()
if d <= 0 {
d = 5 * 60 * 1000

View File

@@ -61,12 +61,15 @@ func Exec(qt *querytracer.Tracer, ec *EvalConfig, q string, isFirstPointOnly boo
}
}
var rv []*timeseries
qid := activeQueriesV.Add(ec, q)
rv, err := evalExpr(qt, ec, e)
rv, err = evalExpr(qt, ec, e)
activeQueriesV.Remove(qid)
if err != nil {
return nil, err
}
if isFirstPointOnly {
// Remove all the points except the first one from every time series.
for _, ts := range rv {
@@ -325,3 +328,23 @@ func escapeDots(s string) string {
}
return string(result)
}
// ExtractMetricsFromQuery visits all the expressions in query and returns all the metrics found in the query.
func ExtractMetricsFromQuery(query string) ([]string, error) {
expr, err := metricsql.Parse(query)
if err != nil {
return nil, fmt.Errorf("error parsing query: %w", err)
}
var metrics []string
metricsql.VisitAll(expr, func(e metricsql.Expr) {
if me, ok := e.(*metricsql.MetricExpr); ok {
metricStr := string(me.AppendString(nil))
if metricStr != "" {
metrics = append(metrics, metricStr)
}
}
})
return metrics, nil
}

View File

@@ -0,0 +1,313 @@
package promql
import (
"math"
"slices"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
func TestSimulatedExec(t *testing.T) {
accountID := uint32(123)
projectID := uint32(567)
start := int64(1000e3)
end := int64(2000e3)
step := int64(200e3)
// Base EvalConfig that will be copied for each test
baseEC := EvalConfig{
Start: start,
End: end,
Step: step,
MaxPointsPerSeries: 1e4,
MaxSeries: 1000,
Deadline: searchutil.NewDeadline(time.Now(), time.Hour, ""),
RoundDigits: 100,
MayCache: false,
}
t.Run(`simple_metric_exact_match`, func(t *testing.T) {
t.Skip()
ec := copyEvalConfig(&baseEC)
mn := newMetric(accountID, projectID,
"__name__", "test_metric",
"a", "b",
)
ec.SimulatedSamples = []*storage.SimulatedSamples{mn.build()}
q := `test_metric{a="b"}`
result, err := Exec(nil, ec, q, false)
if err != nil {
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
}
// Expected result
expectedMN := storage.MetricName{
MetricGroup: []byte("test_metric"),
Tags: []storage.Tag{
{
Key: []byte("a"),
Value: []byte("b"),
},
},
}
expectedResult := []netstorage.Result{
{
MetricName: expectedMN,
Values: mn.Value,
Timestamps: mn.Timestamps,
},
}
testResultsEqual(t, result, expectedResult)
})
t.Run(`filtered_by_tag_value`, func(t *testing.T) {
t.Skip()
// Create a copy of base EvalConfig
ec := copyEvalConfig(&baseEC)
mn := metricBuilders{
newMetric(accountID, projectID,
"__name__", "test_metric",
"a", "b",
"region", "us-west",
),
newMetric(accountID, projectID,
"__name__", "test_metric",
"a", "b",
"region", "us-east",
),
}
ec.SimulatedSamples = mn.build()
q := `test_metric{region="us-west"}`
result, err := Exec(nil, ec, q, false)
if err != nil {
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
}
// Expected result
expectedMN := storage.MetricName{
MetricGroup: []byte("test_metric"),
Tags: []storage.Tag{
{
Key: []byte("a"),
Value: []byte("b"),
},
{
Key: []byte("region"),
Value: []byte("us-west"),
},
},
}
expectedResult := []netstorage.Result{
{
MetricName: expectedMN,
Values: mn[0].Value,
Timestamps: mn[0].Timestamps,
},
}
testResultsEqual(t, result, expectedResult)
})
t.Run(`regex_match_on_tag`, func(t *testing.T) {
ec := copyEvalConfig(&baseEC)
mn := metricBuilders{
newMetric(accountID, projectID,
"__name__", "test_metric",
"env", "prod",
),
newMetric(accountID, projectID,
"__name__", "test_metric",
"env", "staging",
),
newMetric(accountID, projectID,
"__name__", "test_metric",
"env", "dev",
),
}
ec.SimulatedSamples = mn.build()
q := `test_metric{env=~"prod|staging"}`
result, err := Exec(nil, ec, q, false)
if err != nil {
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
}
expectedResult := []netstorage.Result{mn[0].toResult(), mn[1].toResult()}
testResultsEqual(t, result, expectedResult)
})
}
func TestSumOverTime(t *testing.T) {
accountID := uint32(123)
projectID := uint32(567)
start := int64(1000e3)
end := int64(1300e3)
step := int64(30e3)
baseEC := EvalConfig{
Start: start,
End: end,
Step: step,
MaxPointsPerSeries: 1e4,
MaxSeries: 1000,
Deadline: searchutil.NewDeadline(time.Now(), time.Hour, ""),
RoundDigits: 100,
MayCache: false,
}
t.Run(`basic_sum_over_time`, func(t *testing.T) {
ec := copyEvalConfig(&baseEC)
metric := newMetric(accountID, projectID,
"__name__", "test_metric",
"app", "api-server",
).withValues(1, 2, 3, 4, 5, 6).withUnix(1000, 1015, 1030, 1045, 1060, 1075)
ec.SimulatedSamples = []*storage.SimulatedSamples{metric.build()}
q := `sum_over_time(test_metric[30s])`
result, err := Exec(nil, ec, q, false)
if err != nil {
t.Fatalf(`unexpected error when executing %q: %s`, q, err)
}
expectedResult := []netstorage.Result{
newMetric(accountID, projectID,
"app", "api-server",
).withValues(1, 5, 9, 6).withUnix(1000, 1030, 1060, 1090).toResult(),
}
testSimulatedResultsEqual(t, result, expectedResult)
})
}
type metricBuilder storage.SimulatedSamples
func newMetric(accountID uint32, projectID uint32, pairs ...string) *metricBuilder {
mn := storage.MetricName{}
for i := 0; i < len(pairs); i += 2 {
mn.AddTag(pairs[i], pairs[i+1])
}
return &metricBuilder{
Name: mn,
Value: []float64{10, 20, 30, 40, 50, 60},
Timestamps: []int64{1000e3, 1200e3, 1400e3, 1600e3, 1800e3, 2000e3},
}
}
func (b *metricBuilder) withUnix(unix ...int64) *metricBuilder {
b.Timestamps = make([]int64, len(unix))
for i := range unix {
b.Timestamps[i] = unix[i] * 1e3
}
return b
}
func (b *metricBuilder) withValues(values ...float64) *metricBuilder {
b.Value = values
return b
}
func (b *metricBuilder) build() *storage.SimulatedSamples {
return (*storage.SimulatedSamples)(b)
}
func (b *metricBuilder) toResult() netstorage.Result {
return netstorage.Result{
MetricName: b.Name,
Values: b.Value,
Timestamps: b.Timestamps,
}
}
type metricBuilders []*metricBuilder
func (b metricBuilders) build() []*storage.SimulatedSamples {
ss := make([]*storage.SimulatedSamples, len(b))
for i := range b {
ss[i] = b[i].build()
}
return ss
}
func testSimulatedResultsEqual(t *testing.T, result, resultExpected []netstorage.Result) {
t.Helper()
result = removeEmptyValuesAndTimeseries(result)
if len(result) != len(resultExpected) {
t.Fatalf(`unexpected timeseries count; got %d; want %d`, len(result), len(resultExpected))
}
for i := range result {
r := &result[i]
rExpected := &resultExpected[i]
testMetricNamesEqual(t, &r.MetricName, &rExpected.MetricName, i)
testRowsEqual(t, r.Values, r.Timestamps, rExpected.Values, rExpected.Timestamps)
}
}
func removeEmptyValuesAndTimeseries(tss []netstorage.Result) []netstorage.Result {
dst := tss[:0]
for i := range tss {
ts := &tss[i]
hasNaNs := slices.ContainsFunc(ts.Values, math.IsNaN)
if !hasNaNs {
// Fast path: nothing to remove.
if len(ts.Values) > 0 {
dst = append(dst, *ts)
}
continue
}
// Slow path: remove NaNs.
srcTimestamps := ts.Timestamps
dstValues := ts.Values[:0]
// Do not reuse ts.Timestamps for dstTimestamps, since ts.Timestamps
// may be shared among multiple time series.
dstTimestamps := make([]int64, 0, len(ts.Timestamps))
for j, v := range ts.Values {
if math.IsNaN(v) {
continue
}
dstValues = append(dstValues, v)
dstTimestamps = append(dstTimestamps, srcTimestamps[j])
}
ts.Values = dstValues
ts.Timestamps = dstTimestamps
if len(ts.Values) > 0 {
dst = append(dst, *ts)
}
}
return dst
}
func TestExtractMetricsFromQuery(t *testing.T) {
query := `(vm_free_disk_space_bytes{job=~"$job", instance=~"$instance"}-vm_free_disk_space_limit_bytes{job=~"$job", instance=~"$instance"})
/
ignoring(path) (
(rate(vm_rows_added_to_storage_total{job=~"$job", instance=~"$instance"}[1d]) -
sum(rate(vm_deduplicated_samples_total{job=~"$job", instance=~"$instance"}[1d])) without (type)) *
(
sum(vm_data_size_bytes{job=~"$job", instance=~"$instance", type!~"indexdb.*"}) without(type) /
sum(vm_rows{job=~"$job", instance=~"$instance", type!~"indexdb.*"}) without(type)
)
+
rate(vm_new_timeseries_created_total{job=~"$job", instance=~"$instance"}[1d]) *
(
sum(vm_data_size_bytes{job=~"$job", instance=~"$instance", type="indexdb/file"}) /
sum(vm_rows{job=~"$job", instance=~"$instance", type="indexdb/file"})
)
)`
metrics, err := ExtractMetricsFromQuery(query)
if err != nil {
t.Fatalf(`unexpected error when extracting metrics from query: %s`, err)
}
t.Logf(`metrics: %v`, metrics)
}

View File

@@ -1,12 +1,12 @@
package promql
import (
"flag"
"fmt"
"math"
"strconv"
"strings"
"sync"
"time"
"github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/metricsql"
@@ -17,10 +17,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
var minStalenessInterval = flag.Duration("search.minStalenessInterval", 0, "The minimum interval for staleness calculations. "+
"This flag could be useful for removing gaps on graphs generated from time series with irregular intervals between samples. "+
"See also '-search.maxStalenessInterval'")
var rollupFuncs = map[string]newRollupFunc{
"absent_over_time": newRollupFuncOneArg(rollupAbsent),
"aggr_over_time": newRollupFuncTwoArgs(rollupFake),
@@ -372,7 +368,7 @@ func getRollupTag(expr metricsql.Expr) (string, error) {
}
func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start, end, step int64, maxPointsPerSeries int,
window, lookbackDelta int64, sharedTimestamps []int64) (
window, lookbackDelta int64, sharedTimestamps []int64, minStalenessInterval time.Duration) (
func(values []float64, timestamps []int64), []*rollupConfig, error) {
preFunc := func(_ []float64, _ []int64) {}
funcName = strings.ToLower(funcName)
@@ -408,6 +404,7 @@ func getRollupConfigs(funcName string, rf rollupFunc, expr metricsql.Expr, start
Timestamps: sharedTimestamps,
isDefaultRollup: funcName == "default_rollup",
samplesScannedPerCall: samplesScannedPerCall,
minStalenessInterval: minStalenessInterval,
}
}
@@ -600,6 +597,9 @@ type rollupConfig struct {
//
// If zero, then it is considered that Func scans all the samples passed to it.
samplesScannedPerCall int
// The minimum interval for staleness calculations.
minStalenessInterval time.Duration
}
func (rc *rollupConfig) getTimestamps() []int64 {
@@ -723,8 +723,8 @@ func (rc *rollupConfig) doInternal(dstValues []float64, tsm *timeseriesMap, valu
if rc.LookbackDelta > 0 && maxPrevInterval > rc.LookbackDelta {
maxPrevInterval = rc.LookbackDelta
}
if *minStalenessInterval > 0 {
if msi := minStalenessInterval.Milliseconds(); msi > 0 && maxPrevInterval < msi {
if rc.minStalenessInterval > 0 {
if msi := rc.minStalenessInterval.Milliseconds(); msi > 0 && maxPrevInterval < msi {
maxPrevInterval = msi
}
}
@@ -820,11 +820,17 @@ func seekFirstTimestampIdxAfter(timestamps []int64, seekTimestamp int64, nHint i
if len(timestamps) == 0 || timestamps[0] > seekTimestamp {
return 0
}
startIdx := max(nHint-2, 0)
startIdx := nHint - 2
if startIdx < 0 {
startIdx = 0
}
if startIdx >= len(timestamps) {
startIdx = len(timestamps) - 1
}
endIdx := min(nHint+2, len(timestamps))
endIdx := nHint + 2
if endIdx > len(timestamps) {
endIdx = len(timestamps)
}
if startIdx > 0 && timestamps[startIdx] <= seekTimestamp {
timestamps = timestamps[startIdx:]
endIdx -= startIdx

View File

@@ -7,12 +7,10 @@ import (
"strings"
"time"
"github.com/VictoriaMetrics/metricsql"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputil"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/metricsql"
)
var (
@@ -22,7 +20,6 @@ var (
maxStatusRequestDuration = flag.Duration("search.maxStatusRequestDuration", time.Minute*5, "The maximum duration for /api/v1/status/* requests")
maxLabelsAPIDuration = flag.Duration("search.maxLabelsAPIDuration", time.Second*5, "The maximum duration for /api/v1/labels, /api/v1/label/.../values and /api/v1/series requests. "+
"See also -search.maxLabelsAPISeries and -search.ignoreExtraFiltersAtLabelsAPI")
maxQueryLen = flagutil.NewBytes("search.maxQueryLen", 16*1024, "The maximum search query length in bytes")
)
// GetMaxQueryDuration returns the maximum duration for query from r.
@@ -230,8 +227,3 @@ func toTagFilter(dst *storage.TagFilter, src *metricsql.LabelFilter) {
dst.IsRegexp = src.IsRegexp
dst.IsNegative = src.IsNegative
}
// GetMaxQueryLen returns the current value of the search.maxQueryLen flag.
func GetMaxQueryLen() int {
return maxQueryLen.IntN()
}

View File

@@ -21,7 +21,7 @@ However, there are some [intentional differences](https://medium.com/@romanhavro
[Standalone MetricsQL package](https://godoc.org/github.com/VictoriaMetrics/metricsql) can be used for parsing MetricsQL in external apps.
If you are unfamiliar with PromQL, we suggest reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
If you are unfamiliar with PromQL, then it is suggested reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
and introduction into [basic querying via MetricsQL](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#metricsql).
The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
@@ -69,13 +69,13 @@ The list of MetricsQL features on top of PromQL:
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
* The lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
and the real interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) (aka `scrape_interval`).
For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
The difference is documented in [rate() docs](#rate).
* Numeric values may include underscore delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
* Numeric values can contain `_` delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
* [Series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
See [these docs](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering-by-multiple-or-filters) for details.
@@ -111,8 +111,8 @@ The list of MetricsQL features on top of PromQL:
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
Additionally, the following escape sequences are supported:
* `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
* `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
- `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
- `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
* Aggregate functions support optional `limit N` suffix in order to limit the number of output series.
For example, `sum(x) by (y) limit 3` limits the number of output time series after the aggregation to 3.
All the other time series are dropped.
@@ -138,9 +138,8 @@ This may result in `duplicate time series` error when the function is applied to
This error can be fixed by applying `keep_metric_names` modifier to the function or binary operator.
For example:
* `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
* `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
- `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
- `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
## MetricsQL functions
@@ -167,10 +166,10 @@ Additional details:
* If the given [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) returns multiple time series,
then rollups are calculated individually per each returned series.
* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
* Every [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) in MetricsQL must be wrapped into a rollup function.
Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
@@ -667,9 +666,8 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
`outlier_iqr_over_time(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last sample on the given lookbehind window `d`
if its value is either smaller than the `q25-1.5*iqr` or bigger than `q75+1.5*iqr` where:
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
The `outlier_iqr_over_time()` is useful for detecting anomalies in gauge values based on the previous history of values.
For example, `outlier_iqr_over_time(memory_usage_bytes[1h])` triggers when `memory_usage_bytes` suddenly goes outside the usual value range for the last hour.
@@ -761,6 +759,7 @@ This function is usually applied to [counters](https://docs.victoriametrics.com/
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
#### rate_over_sum
`rate_over_sum(series_selector[d])` is a [rollup function](#rollup-functions), which calculates per-second rate over the sum of [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
@@ -1107,6 +1106,7 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
See also [zscore](#zscore), [range_trim_zscore](#range_trim_zscore) and [outlier_iqr_over_time](#outlier_iqr_over_time).
### Transform functions
**Transform functions** calculate transformations over [rollup results](#rollup-functions).
@@ -1851,6 +1851,7 @@ The list of supported label manipulation functions:
`alias(q, "name")` is [label manipulation function](#label-manipulation-functions), which sets the given `name` to all the time series returned by `q`.
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
#### drop_common_labels
`drop_common_labels(q1, ...., qN)` is [label manipulation function](#label-manipulation-functions), which drops common `label="value"` pairs
@@ -1876,7 +1877,7 @@ For example, `label_graphite_group({__graphite__="foo*.bar.*"}, 0, 2)` would sub
This function is useful for aggregating Graphite metrics with [aggregate functions](#aggregate-functions). For example, the following query would return per-app memory usage:
```metricsql
```
sum by (__name__) (
label_graphite_group({__graphite__="app*.host*.memory_usage"}, 0)
)
@@ -2002,6 +2003,7 @@ would return series in the following order of `bar` label values: `101`, `15`, `
See also [sort_by_label_numeric](#sort_by_label_numeric) and [sort_by_label_desc](#sort_by_label_desc).
### Aggregate functions
**Aggregate functions** calculate aggregates over groups of [rollup results](#rollup-functions).
@@ -2177,9 +2179,8 @@ per each `group_labels` for all the time series returned by `q`. The aggregate i
`outliers_iqr(q)` is [aggregate function](#aggregate-functions), which returns time series from `q` with at least a single point
outside e.g. [Interquartile range outlier bounds](https://en.wikipedia.org/wiki/Interquartile_range) `[q25-1.5*iqr .. q75+1.5*iqr]`
comparing to other time series at the given point, where:
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
The `outliers_iqr()` is useful for detecting anomalous series in the group of series. For example, `outliers_iqr(temperature) by (country)` returns
per-country series with anomalous outlier values comparing to the rest of per-country series.
@@ -2348,10 +2349,10 @@ VictoriaMetrics performs subqueries in the following way:
VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
* All the [series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering),
which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.

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

@@ -36,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-DK22yiEQ.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
<script type="module" crossorigin src="./assets/index-Ck5nH8JI.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-BVRvRxZ2.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-Ccv_zSYG.css">
<link rel="stylesheet" crossorigin href="./assets/index-BHg4iVVe.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -683,7 +683,7 @@ func writeStorageMetrics(w io.Writer, strg *storage.Storage) {
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="miss_percentage"}`, m.MetricIDCacheMissEvictionBytes)
metrics.WriteCounterUint64(w, `vm_cache_eviction_bytes_total{type="storage/metricIDs", reason="expiration"}`, m.MetricIDCacheExpireEvictionBytes)
metrics.WriteCounterUint64(w, `vm_deleted_metrics_total{type="indexdb"}`, m.DeletedMetricsCount)
metrics.WriteCounterUint64(w, `vm_deleted_metrics_total{type="indexdb"}`, idbm.DeletedMetricsCount)
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/tsid"}`, m.TSIDCacheCollisions)
metrics.WriteCounterUint64(w, `vm_cache_collisions_total{type="storage/metricName"}`, m.MetricNameCacheCollisions)

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine3.22
FROM node:20-alpine3.19
# Sets a custom location for the npm cache, preventing access errors in system directories
ENV NPM_CONFIG_CACHE=/build/.npm

View File

@@ -1177,7 +1177,7 @@
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1188,36 +1188,24 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1262,316 +1250,6 @@
"node": ">= 8"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@preact/preset-vite": {
"version": "2.10.2",
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.2.tgz",
@@ -2072,9 +1750,9 @@
}
},
"node_modules/@types/node": {
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"version": "24.0.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -2543,7 +2221,7 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -2845,7 +2523,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -2894,14 +2572,6 @@
"devOptional": true,
"license": "MIT/X11"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -3032,23 +2702,6 @@
"node": ">= 16"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -3097,14 +2750,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3415,20 +3060,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -4177,7 +3808,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -4891,7 +4522,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4946,7 +4577,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -4985,7 +4616,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -5488,7 +5119,7 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -5553,14 +5184,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/node-html-parser": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
@@ -5889,7 +5512,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -6131,21 +5754,6 @@
"react-dom": ">=18"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -6446,28 +6054,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/sass": {
"version": "1.89.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sass-embedded": {
"version": "1.89.2",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.89.2.tgz",
@@ -6997,29 +6583,6 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/source-map-support/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stack-trace": {
"version": "1.0.0-pre2",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
@@ -7286,26 +6849,6 @@
"node": ">=16.0.0"
}
},
"node_modules/terser": {
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -7416,7 +6959,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"

View File

@@ -18,96 +18,84 @@ import QueryAnalyzer from "./pages/QueryAnalyzer";
import DownsamplingFilters from "./pages/DownsamplingFilters";
import RetentionFilters from "./pages/RetentionFilters";
import RawQueryPage from "./pages/RawQueryPage";
import ExploreRules from "./pages/ExploreAlerts/ExploreRules";
import ExploreNotifiers from "./pages/ExploreAlerts/ExploreNotifiers";
const App: FC = () => {
const [loadedTheme, setLoadedTheme] = useState(false);
return (
<>
<HashRouter>
<AppContextProvider>
<>
<ThemeProvider onLoaded={setLoadedTheme} />
{loadedTheme && (
<Routes>
return <>
<HashRouter>
<AppContextProvider>
<>
<ThemeProvider onLoaded={setLoadedTheme}/>
{loadedTheme && (
<Routes>
<Route
path={"/"}
element={<MainLayout/>}
>
<Route
path={"/"}
element={<MainLayout />}
>
<Route
path={router.home}
element={<CustomPanel />}
/>
<Route
path={router.rawQuery}
element={<RawQueryPage />}
/>
<Route
path={router.metrics}
element={<ExploreMetrics />}
/>
<Route
path={router.cardinality}
element={<CardinalityPanel />}
/>
<Route
path={router.topQueries}
element={<TopQueries />}
/>
<Route
path={router.trace}
element={<TracePage />}
/>
<Route
path={router.queryAnalyzer}
element={<QueryAnalyzer />}
/>
<Route
path={router.dashboards}
element={<DashboardsLayout />}
/>
<Route
path={router.withTemplate}
element={<WithTemplate />}
/>
<Route
path={router.relabel}
element={<Relabel />}
/>
<Route
path={router.activeQueries}
element={<ActiveQueries />}
/>
<Route
path={router.icons}
element={<PreviewIcons />}
/>
<Route
path={router.downsamplingDebug}
element={<DownsamplingFilters />}
/>
<Route
path={router.retentionDebug}
element={<RetentionFilters />}
/>
<Route
path={router.rules}
element={<ExploreRules />}
/>
<Route
path={router.notifiers}
element={<ExploreNotifiers />}
/>
</Route>
</Routes>
)}
</>
</AppContextProvider>
</HashRouter>
</>
);
path={router.home}
element={<CustomPanel/>}
/>
<Route
path={router.rawQuery}
element={<RawQueryPage/>}
/>
<Route
path={router.metrics}
element={<ExploreMetrics/>}
/>
<Route
path={router.cardinality}
element={<CardinalityPanel/>}
/>
<Route
path={router.topQueries}
element={<TopQueries/>}
/>
<Route
path={router.trace}
element={<TracePage/>}
/>
<Route
path={router.queryAnalyzer}
element={<QueryAnalyzer/>}
/>
<Route
path={router.dashboards}
element={<DashboardsLayout/>}
/>
<Route
path={router.withTemplate}
element={<WithTemplate/>}
/>
<Route
path={router.relabel}
element={<Relabel/>}
/>
<Route
path={router.activeQueries}
element={<ActiveQueries/>}
/>
<Route
path={router.icons}
element={<PreviewIcons/>}
/>
<Route
path={router.downsamplingDebug}
element={<DownsamplingFilters/>}
/>
<Route
path={router.retentionDebug}
element={<RetentionFilters/>}
/>
</Route>
</Routes>
)}
</>
</AppContextProvider>
</HashRouter>
</>;
};
export default App;

View File

@@ -1,2 +1,2 @@
import { getUrlWithoutTenant } from "../utils/tenants";
export const getAccountIds = (server: string) => `${getUrlWithoutTenant(server)}/admin/tenants`;
export const getAccountIds = (server: string) =>
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;

View File

@@ -1,23 +0,0 @@
export const getGroupsUrl = (server: string): string => {
return `${server}/vmalert/api/v1/rules?datasource_type=prometheus`;
};
export const getItemUrl = (
server: string,
groupId: string,
id: string,
mode: string,
): string => {
return `${server}/vmalert/api/v1/${mode}?group_id=${groupId}&${mode}_id=${id}`;
};
export const getGroupUrl = (
server: string,
id: string,
): string => {
return `${server}/vmalert/api/v1/group?group_id=${id}`;
};
export const getNotifiersUrl = (server: string): string => {
return `${server}/vmalert/api/v1/notifiers`;
};

View File

@@ -21,7 +21,7 @@ However, there are some [intentional differences](https://medium.com/@romanhavro
[Standalone MetricsQL package](https://godoc.org/github.com/VictoriaMetrics/metricsql) can be used for parsing MetricsQL in external apps.
If you are unfamiliar with PromQL, we suggest reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
If you are unfamiliar with PromQL, then it is suggested reading [this tutorial for beginners](https://medium.com/@valyala/promql-tutorial-for-beginners-9ab455142085)
and introduction into [basic querying via MetricsQL](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#metricsql).
The following functionality is implemented differently in MetricsQL compared to PromQL. This improves user experience:
@@ -69,13 +69,13 @@ The list of MetricsQL features on top of PromQL:
See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#selecting-graphite-metrics).
VictoriaMetrics can be used as Graphite datasource in Grafana. See [these docs](https://docs.victoriametrics.com/victoriametrics/integrations/graphite/#graphite-api-usage) for details.
See also [label_graphite_group](#label_graphite_group) function, which can be used for extracting the given groups from Graphite metric name.
* The lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
* Lookbehind window in square brackets for [rollup functions](#rollup-functions) may be omitted. VictoriaMetrics automatically selects the lookbehind window
depending on the `step` query arg passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query)
and the real interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) (aka `scrape_interval`).
For instance, the following query is valid in VictoriaMetrics: `rate(node_network_receive_bytes_total)`.
It is roughly equivalent to `rate(node_network_receive_bytes_total[$__interval])` when used in Grafana.
The difference is documented in [rate() docs](#rate).
* Numeric values may include underscore delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
* Numeric values can contain `_` delimiters for better readability. For example, `1_234_567_890` can be used in queries instead of `1234567890`.
* [Series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) accept multiple `or` filters. For example, `{env="prod",job="a" or env="dev",job="b"}`
selects series with `{env="prod",job="a"}` or `{env="dev",job="b"}` labels.
See [these docs](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering-by-multiple-or-filters) for details.
@@ -111,8 +111,8 @@ The list of MetricsQL features on top of PromQL:
* Metric names and labels names may contain escaped chars. For example, `foo\-bar{baz\=aa="b"}` is valid expression.
It returns time series with name `foo-bar` containing label `baz=aa` with value `b`.
Additionally, the following escape sequences are supported:
* `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
* `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
- `\xXX`, where `XX` is hexadecimal representation of the escaped ascii char.
- `\uXXXX`, where `XXXX` is a hexadecimal representation of the escaped unicode char.
* Aggregate functions support optional `limit N` suffix in order to limit the number of output series.
For example, `sum(x) by (y) limit 3` limits the number of output time series after the aggregation to 3.
All the other time series are dropped.
@@ -138,9 +138,8 @@ This may result in `duplicate time series` error when the function is applied to
This error can be fixed by applying `keep_metric_names` modifier to the function or binary operator.
For example:
* `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
* `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
- `rate({__name__=~"foo|bar"}) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
- `({__name__=~"foo|bar"} / 10) keep_metric_names` leaves `foo` and `bar` metric names in the returned time series.
## MetricsQL functions
@@ -167,10 +166,10 @@ Additional details:
* If the given [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) returns multiple time series,
then rollups are calculated individually per each returned series.
* If lookbehind window in square brackets is missing, then it is automatically set to the following value:
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
* Every [series selector](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering) in MetricsQL must be wrapped into a rollup function.
Otherwise, it is automatically wrapped into [default_rollup](#default_rollup). For example, `foo{bar="baz"}`
@@ -667,9 +666,8 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
`outlier_iqr_over_time(series_selector[d])` is a [rollup function](#rollup-functions), which returns the last sample on the given lookbehind window `d`
if its value is either smaller than the `q25-1.5*iqr` or bigger than `q75+1.5*iqr` where:
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) over [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples) on the lookbehind window `d`.
The `outlier_iqr_over_time()` is useful for detecting anomalies in gauge values based on the previous history of values.
For example, `outlier_iqr_over_time(memory_usage_bytes[1h])` triggers when `memory_usage_bytes` suddenly goes outside the usual value range for the last hour.
@@ -761,6 +759,7 @@ This function is usually applied to [counters](https://docs.victoriametrics.com/
See also [increase_prometheus](#increase_prometheus) and [rate](#rate).
#### rate_over_sum
`rate_over_sum(series_selector[d])` is a [rollup function](#rollup-functions), which calculates per-second rate over the sum of [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
@@ -1107,6 +1106,7 @@ This function is usually applied to [gauges](https://docs.victoriametrics.com/vi
See also [zscore](#zscore), [range_trim_zscore](#range_trim_zscore) and [outlier_iqr_over_time](#outlier_iqr_over_time).
### Transform functions
**Transform functions** calculate transformations over [rollup results](#rollup-functions).
@@ -1851,6 +1851,7 @@ The list of supported label manipulation functions:
`alias(q, "name")` is [label manipulation function](#label-manipulation-functions), which sets the given `name` to all the time series returned by `q`.
For example, `alias(up, "foobar")` would rename `up` series to `foobar` series.
#### drop_common_labels
`drop_common_labels(q1, ...., qN)` is [label manipulation function](#label-manipulation-functions), which drops common `label="value"` pairs
@@ -1876,7 +1877,7 @@ For example, `label_graphite_group({__graphite__="foo*.bar.*"}, 0, 2)` would sub
This function is useful for aggregating Graphite metrics with [aggregate functions](#aggregate-functions). For example, the following query would return per-app memory usage:
```metricsql
```
sum by (__name__) (
label_graphite_group({__graphite__="app*.host*.memory_usage"}, 0)
)
@@ -2002,6 +2003,7 @@ would return series in the following order of `bar` label values: `101`, `15`, `
See also [sort_by_label_numeric](#sort_by_label_numeric) and [sort_by_label_desc](#sort_by_label_desc).
### Aggregate functions
**Aggregate functions** calculate aggregates over groups of [rollup results](#rollup-functions).
@@ -2177,9 +2179,8 @@ per each `group_labels` for all the time series returned by `q`. The aggregate i
`outliers_iqr(q)` is [aggregate function](#aggregate-functions), which returns time series from `q` with at least a single point
outside e.g. [Interquartile range outlier bounds](https://en.wikipedia.org/wiki/Interquartile_range) `[q25-1.5*iqr .. q75+1.5*iqr]`
comparing to other time series at the given point, where:
* `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
* `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
- `iqr` is an [Interquartile range](https://en.wikipedia.org/wiki/Interquartile_range) calculated independently per each point on the graph across `q` series.
- `q25` and `q75` are 25th and 75th [percentiles](https://en.wikipedia.org/wiki/Percentile) calculated independently per each point on the graph across `q` series.
The `outliers_iqr()` is useful for detecting anomalous series in the group of series. For example, `outliers_iqr(temperature) by (country)` returns
per-country series with anomalous outlier values comparing to the rest of per-country series.
@@ -2348,10 +2349,10 @@ VictoriaMetrics performs subqueries in the following way:
VictoriaMetrics performs the following implicit conversions for incoming queries before starting the calculations:
* If lookbehind window in square brackets is missing inside [rollup function](#rollup-functions), then it is automatically set to the following value:
* To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
- To `step` value passed to [/api/v1/query_range](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#range-query) or [/api/v1/query](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#instant-query)
for all the [rollup functions](#rollup-functions) except of [default_rollup](#default_rollup) and [rate](#rate). This value is known as `$__interval` in Grafana or `1i` in MetricsQL.
For example, `avg_over_time(temperature)` is automatically transformed to `avg_over_time(temperature[1i])`.
* To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
- To the `max(step, scrape_interval)`, where `scrape_interval` is the interval between [raw samples](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#raw-samples)
for [default_rollup](#default_rollup) and [rate](#rate) functions. This allows avoiding unexpected gaps on the graph when `step` is smaller than `scrape_interval`.
* All the [series selectors](https://docs.victoriametrics.com/victoriametrics/keyconcepts/#filtering),
which aren't wrapped into [rollup functions](#rollup-functions), are automatically wrapped into [default_rollup](#default_rollup) function.

View File

@@ -30,13 +30,7 @@ const delayOptions: AutoRefreshOption[] = [
{ seconds: 7200, title: "2h" }
];
interface ExecutionControlsProps {
tooltip: string;
useAutorefresh?: boolean;
closeModal: () => void;
}
export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAutorefresh, closeModal }) => {
export const ExecutionControls: FC = () => {
const { isMobile } = useDeviceDetect();
const dispatch = useTimeDispatch();
@@ -62,9 +56,6 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
const handleUpdate = () => {
dispatch({ type: "RUN_QUERY" });
if (!useAutorefresh && isMobile) {
closeModal();
}
};
useEffect(() => {
@@ -86,118 +77,91 @@ export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAuto
handleChange(d);
};
return (
<>
<div className="vm-execution-controls">
<div
className={classNames({
"vm-execution-controls-buttons": true,
"vm-execution-controls-buttons_mobile": isMobile,
"vm-header-button": !appModeEnable,
"vm-autorefresh": useAutorefresh,
})}
>
{useAutorefresh ? (
isMobile ? (
<div
className="vm-mobile-option"
onClick={toggleOpenOptions}
>
<span className="vm-mobile-option__icon"><RestartIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Auto-refresh</span>
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<>
<Tooltip title={tooltip}>
<Button
variant="contained"
color="primary"
onClick={handleUpdate}
startIcon={<RefreshIcon/>}
ariaLabel={tooltip}
/>
</Tooltip>
<Tooltip title="Auto-refresh control">
<div ref={optionsButtonRef}>
<Button
variant="contained"
color="primary"
fullWidth
endIcon={(
<div
className={classNames({
"vm-execution-controls-buttons__arrow": true,
"vm-execution-controls-buttons__arrow_open": openOptions,
})}
>
<ArrowDownIcon/>
</div>
)}
onClick={toggleOpenOptions}
>
{selectedDelay.title}
</Button>
</div>
</Tooltip>
</>
)
) : (
isMobile ? (
<div
className="vm-mobile-option"
onClick={handleUpdate}
>
<span className="vm-mobile-option__icon"><RestartIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Refresh</span>
</div>
</div>
) : (
return <>
<div className="vm-execution-controls">
<div
className={classNames({
"vm-execution-controls-buttons": true,
"vm-execution-controls-buttons_mobile": isMobile,
"vm-header-button": !appModeEnable,
})}
>
{!isMobile && (
<Tooltip title="Refresh dashboard">
<Button
variant="contained"
color="primary"
onClick={handleUpdate}
startIcon={<RefreshIcon/>}
ariaLabel="refresh dashboard"
/>
</Tooltip>
)}
{isMobile ? (
<div
className="vm-mobile-option"
onClick={toggleOpenOptions}
>
<span className="vm-mobile-option__icon"><RestartIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Auto-refresh</span>
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Tooltip title="Auto-refresh control">
<div ref={optionsButtonRef}>
<Button
variant="contained"
color="primary"
onClick={handleUpdate}
startIcon={<RefreshIcon/>}
ariaLabel={tooltip}
/>
)
)}
</div>
fullWidth
endIcon={(
<div
className={classNames({
"vm-execution-controls-buttons__arrow": true,
"vm-execution-controls-buttons__arrow_open": openOptions,
})}
>
<ArrowDownIcon/>
</div>
)}
onClick={toggleOpenOptions}
>
{selectedDelay.title}
</Button>
</div>
</Tooltip>
)}
</div>
{useAutorefresh && (
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
title={isMobile ? "Auto-refresh duration" : undefined}
>
</div>
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
title={isMobile ? "Auto-refresh duration" : undefined}
>
<div
className={classNames({
"vm-execution-controls-list": true,
"vm-execution-controls-list_mobile": isMobile,
})}
>
{delayOptions.map(d => (
<div
className={classNames({
"vm-execution-controls-list": true,
"vm-execution-controls-list_mobile": isMobile,
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": d.seconds === selectedDelay.seconds
})}
key={d.seconds}
onClick={createHandlerChange(d)}
>
{delayOptions.map(d => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": d.seconds === selectedDelay.seconds
})}
key={d.seconds}
onClick={createHandlerChange(d)}
>
{d.title}
</div>
))}
{d.title}
</div>
</Popper>
)}
</>
);
))}
</div>
</Popper>
</>;
};

View File

@@ -7,10 +7,7 @@
display: flex;
justify-content: space-between;
border-radius: calc($button-radius + 1px);
:is(.vm-autorefresh) {
min-width: 107px;
}
min-width: 107px;
&_mobile {
flex-direction: column;

View File

@@ -1,34 +0,0 @@
import "./style.scss";
import { ReactNode } from "react";
export type BadgeColor = "firing" | "inactive" | "pending" | "no-match" | "unhealthy" | "ok" | "passive";
interface BadgeItem {
value?: number | string;
color: BadgeColor;
}
interface BadgesProps {
items: Record<string, BadgeItem>;
align?: "center" | "start" | "end";
children?: ReactNode;
}
const Badges = ({ items, children, align = "start" }: BadgesProps) => {
return (
<div
className="vm-badges"
style={{ "justify-content": align }}
>
{Object.entries(items).map(([name, props]) => (
<span
key={name}
className={`vm-badge ${props.color}`}
>{props.value ? `${name}: ${props.value}` : name}</span>
))}
{children}
</div>
);
};
export default Badges;

View File

@@ -1,69 +0,0 @@
@use "src/styles/variables" as *;
$badge-colors: (
"firing": $color-error,
"inactive": $color-success,
"pending": $color-warning,
"no-match": $color-notice,
"unhealthy": $color-broken,
"ok": $color-info,
"passive": $color-passive,
"all": $color-passive,
);
.vm-badges {
display: flex;
flex-wrap: wrap;
gap: $padding-small;
&.align-center {
justify-content: center;
}
.vm-badge {
padding: 0 $padding-tiny;
width: fit-content;
@each $class, $color in $badge-colors {
&.#{$class} {
border: 1px solid $color;
color: $color;
}
}
}
}
.vm-badge-base {
font-weight: 400;
border-radius: $border-radius-small;
}
.vm-badge-menu-item {
@extend .vm-badge-base;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 22px;
@each $class, $color in $badge-colors {
&.#{$class} {
border-right: $border-radius-small solid $color;
}
}
}
.vm-badge-item {
@extend .vm-badge-base;
@each $class, $color in $badge-colors {
&.#{$class} {
border-left: $border-radius-small solid $color;
}
}
}
.vm-badge {
@extend .vm-badge-base;
background-color: transparent;
padding: 0 $padding-tiny;
line-height: 22px;
max-width: 300px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View File

@@ -1,93 +0,0 @@
import "./style.scss";
import { Alert as APIAlert } from "../../../types";
import { createSearchParams } from "react-router-dom";
import Button from "../../Main/Button/Button";
import Badges from "../Badges";
import {
SearchIcon,
} from "../../Main/Icons";
import dayjs from "dayjs";
import CodeExample from "../../Main/CodeExample/CodeExample";
interface BaseAlertProps {
item: APIAlert;
}
const BaseAlert = ({ item }: BaseAlertProps) => {
const query = item?.expression;
const openQueryLink = () => {
const params = {
"g0.expr": query,
"g0.end_time": ""
};
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
};
return (
<div className="vm-explore-alerts-alert-item">
<table>
<tbody>
<tr>
<td
style={{ "text-align": "end" }}
colSpan={2}
>
<Button
size="small"
variant="outlined"
color="gray"
startIcon={<SearchIcon />}
onClick={openQueryLink}
>
<span className="vm-button-text">Run query</span>
</Button>
</td>
</tr>
<tr>
<td className="vm-col-md">Query</td>
<td>
<CodeExample
code={query}
/>
</td>
</tr>
<tr>
<td className="vm-col-md">Active at</td>
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
{!!Object.keys(item?.labels || {}).length && (
<tr>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
</tr>
)}
</tbody>
</table>
{!!Object.keys(item.annotations || {}).length && (
<>
<span className="title">Annotations</span>
<table>
<tbody>
{Object.entries(item.annotations || {}).map(([name, value]) => (
<tr key={name}>
<td className="vm-col-md">{name}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</>
)}
</div>
);
};
export default BaseAlert;

View File

@@ -1,55 +0,0 @@
@use "src/styles/variables" as *;
.vm-modal {
.vm-explore-alerts-alert-item {
table {
width: auto;
}
}
}
.vm-explore-alerts-alert-item {
row-gap: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
.title {
font-weight: bold;
text-align: center;
}
.vm-col-sm {
width: 10%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-col-md {
width: 15%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-button {
color: $color-passive;
border: 1px solid var(--color-passive);
}
table {
width: 100%;
td, th {
line-height: 30px;
padding: 4px $padding-small;
}
th {
font-weight: bold;
text-align: center;
padding: 0 $padding-small;
}
}
}

View File

@@ -1,108 +0,0 @@
import "./style.scss";
import { Group as APIGroup } from "../../../types";
import dayjs from "dayjs";
import { formatDuration } from "../helpers";
import Badges from "../Badges";
interface BaseGroupProps {
group: APIGroup;
}
const BaseGroup = ({ group }: BaseGroupProps) => {
return (
<div className="vm-explore-alerts-group">
<div></div>
<table>
<tbody>
{!!group.interval && (
<tr>
<td className="vm-col-md">Interval</td>
<td>{formatDuration(group.interval)}</td>
</tr>
)}
{!!group.lastEvaluation && (
<tr>
<td className="vm-col-md">Last evaluation</td>
<td>{dayjs(group.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
)}
{!!group.eval_offset && (
<tr>
<td className="vm-col-md">Eval offset</td>
<td>{formatDuration(group.eval_offset)}</td>
</tr>
)}
{!!group.eval_delay && (
<tr>
<td className="vm-col-md">Eval delay</td>
<td>{formatDuration(group.eval_delay)}</td>
</tr>
)}
{!!group.file && (
<tr>
<td className="vm-col-md">File</td>
<td>{group.file}</td>
</tr>
)}
{!!group.concurrency && (
<tr>
<td className="vm-col-md">Concurrency</td>
<td>{group.concurrency}</td>
</tr>
)}
{!!group?.labels?.length && (
<tr>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={Object.fromEntries(Object.entries(group.labels).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
</tr>
)}
{!!group?.params?.length && (
<tr>
<td className="vm-col-md">Params</td>
<td>
<Badges
items={Object.fromEntries(group.params.map(value => [value, {
color: "passive",
}]))}
/>
</td>
</tr>
)}
{!!group?.headers?.length && (
<tr>
<td className="vm-col-md">Headers</td>
<td>
<Badges
items={Object.fromEntries(group.headers.map(value => [value, {
color: "passive",
}]))}
/>
</td>
</tr>
)}
{!!group?.notifier_headers?.length && (
<tr>
<td className="vm-col-md">Notifier headers</td>
<td>
<Badges
items={Object.fromEntries(group.notifier_headers.map(value => [value, {
color: "passive",
}]))}
/>
</td>
</tr>
)}
</tbody>
</table>
</div>
);
};
export default BaseGroup;

View File

@@ -1,78 +0,0 @@
@use "src/styles/variables" as *;
.vm-modal {
.vm-explore-alerts-group {
table {
width: auto;
}
}
}
.vm-explore-alerts-group {
row-gap: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
.title {
font-weight: bold;
text-align: center;
}
pre {
position: relative;
background-color: $color-background-badge;
padding: 0 $padding-global;
border-radius: $border-radius-small;
word-break: break-word;
white-space: pre-wrap;
.keyword,
.function,
.attr-name,
.range-duration {
color: $color-keyword;
}
div {
position: absolute;
top: 0;
right: 0;
display: flex;
column-gap: 4px;
}
}
.vm-col-sm {
width: 10%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-col-md {
width: 15%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
table {
width: 100%;
tr.hoverable {
cursor: pointer;
&:hover {
background-color: $color-background-hover;
}
}
td, th {
line-height: 30px;
padding: 4px $padding-small;
}
th {
font-weight: bold;
text-align: center;
padding: 0 $padding-small;
}
}
}

View File

@@ -1,205 +0,0 @@
import "./style.scss";
import { Rule as APIRule } from "../../../types";
import { useNavigate, createSearchParams } from "react-router-dom";
import { SearchIcon, DetailsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Alert from "../../Main/Alert/Alert";
import Badges, { BadgeColor } from "../Badges";
import dayjs from "dayjs";
import { formatDuration } from "../helpers";
import CodeExample from "../../Main/CodeExample/CodeExample";
interface BaseRuleProps {
item: APIRule;
}
const BaseRule = ({ item }: BaseRuleProps) => {
const query = item?.query;
const navigate = useNavigate();
const openAlertLink = (id: string) => {
return () => {
navigate({
pathname: "/rules",
search: `group_id=${item.group_id}&alert_id=${id}`,
});
};
};
const openQueryLink = () => {
const params = {
"g0.expr": query,
"g0.end_time": ""
};
window.open(`#/?${createSearchParams(params).toString()}`, "_blank", "noopener noreferrer");
};
return (
<div className="vm-explore-alerts-rule-item">
<div></div>
<table>
<tbody>
<tr>
<td
style={{ "text-align": "end" }}
colSpan={2}
>
<Button
size="small"
variant="outlined"
color="gray"
startIcon={<SearchIcon />}
onClick={openQueryLink}
>
<span className="vm-button-text">Run query</span>
</Button>
</td>
</tr>
<tr>
<td className="vm-col-md">Query</td>
<td>
<CodeExample
code={query}
/>
</td>
</tr>
{!!item.duration && (
<tr>
<td className="vm-col-md">For</td>
<td>{formatDuration(item.duration)}</td>
</tr>
)}
{!!item.lastEvaluation && (
<tr>
<td className="vm-col-md">Last evaluation</td>
<td>{dayjs(item.lastEvaluation).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
)}
{!!item.lastError && item.health !== "ok" && (
<tr>
<td className="vm-col-md">Last error</td>
<td>
<Alert variant="error">{item.lastError}</Alert>
</td>
</tr>
)}
{!!Object.keys(item?.labels || {}).length && (
<tr>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
</tr>
)}
</tbody>
</table>
{!!Object.keys(item?.annotations || {}).length && (
<>
<span className="title">Annotations</span>
<table className="fixed">
<tbody>
{Object.entries(item.annotations || {}).map(([name, value]) => (
<tr key={name}>
<td className="vm-col-md">{name}</td>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</>
)}
{!!item?.updates?.length && (
<>
<span className="title">{`Last updates ${item.updates.length}/${item.max_updates_entries}`}</span>
<table className="fixed">
<thead>
<tr>
<th className="vm-col-md">Updated at</th>
<th className="vm-col-md">Series returned</th>
<th className="vm-col-md">Series fetched</th>
<th className="vm-col-md">Duration</th>
<th className="vm-col-md">Executed at</th>
</tr>
</thead>
<tbody>
{item.updates.map((update) => (
<tr
key={update.at}
>
<td className="vm-col-md">{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
<td className="vm-col-md">{update.samples}</td>
<td className="vm-col-md">{update.series_fetched}</td>
<td className="vm-col-md">{formatDuration(update.duration / 1e9)}</td>
<td className="vm-col-md">{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
</tr>
))}
</tbody>
</table>
</>
)}
{!!item?.alerts?.length && (
<>
<span className="title">Alerts</span>
<table className="fixed">
<thead>
<tr>
<th className="vm-col-sm">Active since</th>
<th className="vm-col-sm">State</th>
<th className="vm-col-sm">Value</th>
<th>Labels</th>
<th className="vm-col-hidden"></th>
</tr>
</thead>
<tbody>
{item.alerts.map((alert) => (
<tr
id={`alert-${alert.id}`}
key={alert.id}
>
<td className="vm-col-sm">
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
</td>
<td className="vm-col-sm">
<Badges
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
/>
</td>
<td className="vm-col-sm">
<Badges
items={{ [alert.value]: { color: "passive" } }}
/>
</td>
<td>
<Badges
align="center"
items={Object.fromEntries(Object.entries(alert.labels || {}).map(([name, value]) => [name, {
color: "passive",
value: value,
}]))}
/>
</td>
<td className="vm-col-hidden">
<Button
className="vm-button-borderless"
size="small"
variant="outlined"
color="gray"
startIcon={<DetailsIcon />}
onClick={openAlertLink(alert.id)}
/>
</td>
</tr>
))}
</tbody>
</table>
</>
)}
</div>
);
};
export default BaseRule;

View File

@@ -1,66 +0,0 @@
@use "src/styles/variables" as *;
.vm-modal {
.vm-explore-alerts-rule-item {
table {
width: auto;
}
}
}
.vm-explore-alerts-rule-item {
row-gap: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
.title {
font-weight: bold;
text-align: center;
}
.vm-col-hidden {
width: 30px;
}
.vm-button {
color: $color-passive;
border: 1px solid var(--color-passive);
}
.vm-col-sm {
width: 10%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.vm-col-md {
width: 15%;
white-space: nowrap;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
table {
&.fixed {
table-layout: fixed;
}
width: 100%;
td, th {
line-height: 30px;
padding: 4px $padding-small;
vertical-align: middle;
}
td.align-center {
text-align: center
}
th {
font-weight: bold;
text-align: center;
padding: 0 $padding-small;
}
}
}

View File

@@ -1,57 +0,0 @@
import { FC } from "preact/compat";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { useNavigate } from "react-router-dom";
import { Group as APIGroup } from "../../../types";
import { DetailsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Badges, { BadgeColor } from "../Badges";
import classNames from "classnames";
interface GroupHeaderControlsProps {
group: APIGroup;
}
const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
const { isMobile } = useDeviceDetect();
const navigate = useNavigate();
const openGroupModal = async () => {
navigate({
pathname: "/rules",
search: `group_id=${group.id}`,
});
};
const headerClasses = classNames({
"vm-explore-alerts-group-header": true,
"vm-explore-alerts-group-header_mobile": isMobile,
});
return (
<div className={headerClasses}>
<div className="vm-explore-alerts-group-header__desc">
<div className="vm-explore-alerts-group-header__name">{group.name}</div>
{!isMobile && (
<div className="vm-explore-alerts-group-header__file">{group.file}</div>
)}
</div>
<Badges
items={Object.fromEntries(Object.entries(group.states || {}).map(([name, value]) => [name.toLowerCase(), {
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
value: value,
}]))}
>
<Button
className="vm-button-borderless"
size="small"
color="gray"
variant="outlined"
startIcon={<DetailsIcon />}
onClick={openGroupModal}
/>
</Badges>
</div>
);
};
export default GroupHeaderHeader;

View File

@@ -1,60 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-group-header {
display: flex;
align-items: center;
padding: $padding-tiny 0 $padding-tiny $padding-global;
justify-content: space-between;
.vm-button_small {
padding: 4px;
}
.vm-button-borderless {
border: 0;
}
@media(max-width: 768px) {
.vm-button-text {
display: none;
}
}
&_mobile {
.vm-button-text {
display: none;
}
}
&__desc {
display: flex;
flex-direction: column;
gap: $padding-tiny;
}
&__index {
color: $color-text-secondary;
font-size: $font-size-small;
}
&__name {
flex-grow: 1;
font-weight: bold;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
line-height: 130%;
word-break: break-word;
}
&__file {
color: $color-text-disabled;
}
code {
padding: 0.2em 0.4em;
font-size: 85%;
background-color: $color-hover-black;
border-radius: 6px;
}
}

View File

@@ -1,127 +0,0 @@
import { FC } from "preact/compat";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
import { useAppState } from "../../../state/common/StateContext";
import Tooltip from "../../Main/Tooltip/Tooltip";
import classNames from "classnames";
import { useNavigate } from "react-router-dom";
import Badges, { BadgeColor } from "../Badges";
import {
LinkIcon,
GroupIcon,
AlertIcon,
AlertingRuleIcon,
RecordingRuleIcon,
DetailsIcon,
} from "../../Main/Icons";
import Button from "../../Main/Button/Button";
interface ItemHeaderControlsProps {
entity: string;
type?: string;
groupId: string;
states?: Record<string, number>;
id?: string;
name: string;
onClose?: () => void;
}
const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, type, states, onClose }) => {
const { isMobile } = useDeviceDetect();
const { serverUrl } = useAppState();
const navigate = useNavigate();
const copyToClipboard = useCopyToClipboard();
const openItemLink = () => {
navigate({
pathname: "/rules",
search: `group_id=${groupId}&${entity}_id=${id}`,
});
};
const copyLink = async () => {
let link = `${serverUrl}/vmui/#/rules?group_id=${groupId}`;
if (type) link = `${link}&${entity}_id=${id}`;
await copyToClipboard(link, `Link to ${entity} has been copied`);
};
const headerClasses = classNames({
"vm-explore-alerts-item-header": true,
"vm-explore-alerts-item-header_mobile": isMobile,
});
const renderIcon = () => {
switch(entity) {
case "alert":
return (
<Tooltip title="Alert">
<AlertIcon />
</Tooltip>
);
case "group":
return (
<Tooltip title="Group">
<GroupIcon />
</Tooltip>
);
default:
switch(type) {
case "alerting":
return (
<Tooltip title="Alerting rule">
<AlertingRuleIcon />
</Tooltip>
);
default:
return (
<Tooltip title="Recording rule">
<RecordingRuleIcon />
</Tooltip>
);
}
}
};
return (
<div
className={headerClasses}
id={`rule-${id}`}
>
<div className="vm-explore-alerts-item-header__title">
{renderIcon()}
<div className="vm-explore-alerts-item-header__name">{name}</div>
</div>
<Badges
items={Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
value: value == 1 ? 0 : value,
}]))}
>
{onClose ? (
<Button
className="vm-back-button"
size="small"
variant="outlined"
color="gray"
startIcon={<LinkIcon />}
onClick={copyLink}
>
<span className="vm-button-text">Copy Link</span>
</Button>
) : (
<Button
className="vm-button-borderless"
size="small"
variant="outlined"
color="gray"
startIcon={<DetailsIcon />}
onClick={openItemLink}
/>
)}
</Badges>
</div>
);
};
export default ItemHeader;

View File

@@ -1,70 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-item-header {
display: flex;
grid-template-columns: auto 1fr auto auto;
align-items: center;
justify-content: space-between;
gap: $padding-global;
.vm-button_small {
padding: 4px;
}
@media(max-width: 768px) {
.vm-button-text {
display: none;
}
}
.vm-button-borderless {
border: 0;
}
.vm-back-button {
svg {
transform: rotate(90deg);
}
}
&_mobile {
grid-template-columns: 1fr auto;
.vm-button-text {
display: none;
}
}
&__index {
color: $color-text-secondary;
font-size: $font-size-small;
}
&__name {
font-weight: bold;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
line-height: 130%;
word-break: break-word;
}
&__title {
display: flex;
column-gap: $padding-global;
svg {
fill: $color-text-disabled;
width: 14px;
}
}
&__file {
color: $color-text-disabled;
}
code {
padding: 0.2em 0.4em;
font-size: 85%;
background-color: $color-hover-black;
border-radius: 6px;
}
}

View File

@@ -1,30 +0,0 @@
import { FC } from "preact/compat";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { Notifier } from "../../../types";
import classNames from "classnames";
interface NotifierHeaderControlsProps {
notifier: Notifier;
}
const NotifierHeaderHeader: FC<NotifierHeaderControlsProps> = ({
notifier,
}) => {
const { isMobile } = useDeviceDetect();
return (
<div
className={classNames({
"vm-explore-alerts-notifier-header": true,
"vm-explore-alerts-notifier-header_mobile": isMobile,
})}
>
<div className="vm-explore-alerts-notifier-header__name">
{notifier.kind}
</div>
</div>
);
};
export default NotifierHeaderHeader;

View File

@@ -1,40 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-notifier-header {
display: flex;
grid-template-columns: auto 1fr auto auto;
align-items: center;
padding: $padding-global;
justify-content: space-between;
gap: $padding-global;
&_mobile {
grid-template-columns: 1fr auto;
padding: $padding-small $padding-global;
}
&__index {
color: $color-text-secondary;
font-size: $font-size-small;
}
&__name {
flex-grow: 1;
font-weight: bold;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
line-height: 130%;
}
&__file {
color: $color-text-disabled;
}
code {
padding: 0.2em 0.4em;
font-size: 85%;
background-color: $color-hover-black;
border-radius: 6px;
}
}

View File

@@ -1,59 +0,0 @@
import { FC } from "preact/compat";
import Select from "../../Main/Select/Select";
import { SearchIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface NotifiersHeaderProps {
kinds: string[];
allKinds: string[];
onChangeKinds: (input: string) => void;
onChangeSearch: (input: string) => void;
}
const NotifiersHeader: FC<NotifiersHeaderProps> = ({
kinds,
allKinds,
onChangeKinds,
onChangeSearch,
}) => {
const { isMobile } = useDeviceDetect();
return (
<>
<div
className={classNames({
"vm-explore-alerts-header": true,
"vm-explore-alerts-header_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-explore-alerts-header__rule_type">
<Select
value={kinds}
list={allKinds}
label="Notifier type"
placeholder="Please select notifier type"
onChange={onChangeKinds}
autofocus={!!kinds.length && !isMobile}
includeAll
searchable
/>
</div>
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"
placeholder="Filter by kind, address or labels"
startIcon={<SearchIcon />}
onChange={onChangeSearch}
/>
</div>
</div>
</>
);
};
export default NotifiersHeader;

View File

@@ -1,65 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: $padding-global calc($padding-small + 10px);
width: 100%;
&_mobile {
flex-direction: column;
align-items: stretch;
}
&__rule_type {
min-width: 120px;
}
&__state {
min-width: 150px;
}
&-description {
display: grid;
grid-template-columns: 1fr auto;
align-items: flex-start;
gap: $padding-small;
ul {
list-style-position: inside;
}
button {
color: inherit;
min-height: 29px;
}
code {
margin: 0 3px;
}
}
&-search {
flex-grow: 1;
.vm-text-field__input {
padding: 11px 28px;
}
.vm-text-field__icon-start {
height: 42px;
}
}
&__clear-icon {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
cursor: pointer;
&:hover {
opacity: 0.7;
}
}
}

View File

@@ -1,34 +0,0 @@
import { FC } from "preact/compat";
import ItemHeader from "../ItemHeader";
import Accordion from "../../Main/Accordion/Accordion";
import "./style.scss";
import { Rule as APIRule } from "../../../types";
import BaseRule from "../BaseRule";
interface RuleProps {
states: Record<string, number>;
rule: APIRule;
}
const Rule: FC<RuleProps> = ({ states, rule }) => {
const state = Object.keys(states).length > 0 ? Object.keys(states)[0] : "ok";
return (
<div className={`vm-explore-alerts-rule vm-badge-item ${state.replace(" ", "-")}`}>
<Accordion
key={`rule-${rule.id}`}
title={<ItemHeader
entity="rule"
type={rule.type}
groupId={rule.group_id}
states={states}
id={rule.id}
name={rule.name}
/>}
>
<BaseRule item={rule} />
</Accordion>
</div>
);
};
export default Rule;

View File

@@ -1,18 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-rule {
padding: $padding-tiny;
padding-right: 0;
display: flex;
row-gap: $padding-tiny;
flex-direction: column;
position: relative;
&:has(>details[open]) {
background-color: $color-background-item;
}
&:hover {
background-color: $color-background-item;
}
}

View File

@@ -1,82 +0,0 @@
import { FC, useMemo } from "preact/compat";
import Select from "../../Main/Select/Select";
import { SearchIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface RulesHeaderProps {
types: string[];
allTypes: string[];
allStates: string[];
states: string[];
onChangeTypes: (input: string) => void;
onChangeStates: (input: string) => void;
onChangeSearch: (input: string) => void;
}
const RulesHeader: FC<RulesHeaderProps> = ({
types,
allTypes,
allStates,
states,
onChangeTypes,
onChangeStates,
onChangeSearch,
}) => {
const noStateText = useMemo(
() => (types.length ? "" : "No states. Please select rule states"),
[types],
);
const { isMobile } = useDeviceDetect();
return (
<>
<div
className={classNames({
"vm-explore-alerts-header": true,
"vm-explore-alerts-header_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-explore-alerts-header__rule_type">
<Select
value={types}
list={allTypes}
label="Rules type"
placeholder="Please select rule type"
onChange={onChangeTypes}
autofocus={!!types.length && !isMobile}
includeAll
searchable
/>
</div>
<div className="vm-explore-alerts-header__state">
<Select
itemClassName="vm-badge-menu-item"
value={states}
list={allStates}
label="State"
placeholder="Please rule state"
onChange={onChangeStates}
noOptionsText={noStateText}
includeAll
searchable
/>
</div>
<div className="vm-explore-alerts-header-search">
<TextField
label="Search"
placeholder="Filter by rule, name or labels"
startIcon={<SearchIcon />}
onChange={onChangeSearch}
/>
</div>
</div>
</>
);
};
export default RulesHeader;

View File

@@ -1,65 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: $padding-global calc($padding-small + 10px);
width: 100%;
&_mobile {
flex-direction: column;
align-items: stretch;
}
&__rule_type {
min-width: 120px;
}
&__state {
min-width: 150px;
}
&-description {
display: grid;
grid-template-columns: 1fr auto;
align-items: flex-start;
gap: $padding-small;
ul {
list-style-position: inside;
}
button {
color: inherit;
min-height: 29px;
}
code {
margin: 0 3px;
}
}
&-search {
flex-grow: 1;
.vm-text-field__input {
padding: 11px 28px;
}
.vm-text-field__icon-start {
height: 42px;
}
}
&__clear-icon {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
cursor: pointer;
&:hover {
opacity: 0.7;
}
}
}

View File

@@ -1,58 +0,0 @@
import { FC } from "preact/compat";
import "./style.scss";
import { Target as APITarget } from "../../../types";
import Alert from "../../Main/Alert/Alert";
import Accordion from "../../Main/Accordion/Accordion";
import Badges from "../Badges";
interface TargetProps {
target: APITarget;
}
const Target: FC<TargetProps> = ({ target }) => {
const state = target?.lastError ? "unhealthy" : "ok";
return (
<div className={`vm-explore-alerts-target vm-badge-item ${state.replace(" ", "-")}`}>
{(!!target?.labels?.length || !!target?.lastError) ? (
<Accordion
key={`target-${target.address}`}
title={(
<div className="vm-explore-alerts-target-header__name">{target.address}</div>
)}
>
<div className="vm-explore-alerts-target-item">
<table>
<tbody>
{!!target?.labels?.length && (
<tr>
<td className="vm-col-md">Labels</td>
<td>
<Badges
items={Object.fromEntries(Object.entries(target.labels).map(([name, value]) => [name, {
value: value,
color: "passive",
}]))}
/>
</td>
</tr>
)}
{!!target.lastError && (
<tr>
<td className="vm-col-md">Last error</td>
<td>
<Alert variant="error">{target.lastError}</Alert>
</td>
</tr>
)}
</tbody>
</table>
</div>
</Accordion>
) : (
<span>{target.address}</span>
)}
</div>
);
};
export default Target;

View File

@@ -1,48 +0,0 @@
@use "src/styles/variables" as *;
.vm-explore-alerts-target {
row-gap: $padding-global;
margin-right: $padding-global;
display: flex;
flex-direction: column;
.vm-col-md {
width: 40%;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
table {
width: 100%;
td {
vertical-align: middle;
padding: $padding-global $padding-small;
}
th {
font-weight: bold;
text-align: center;
padding: 0 $padding-small;
}
}
padding: $padding-tiny;
padding-right: 0;
display: flex;
row-gap: $padding-tiny;
flex-direction: column;
position: relative;
border-radius: $border-radius-small;
&:has(>details[open]) {
background-color: $color-background-item;
}
&:hover {
background-color: $color-background-item;
}
.vm-explore-alerts-item-header__name {
line-height: 22px;
}
}

View File

@@ -1,15 +0,0 @@
import dayjs from "dayjs";
export const formatDuration = (raw: number) => {
const duration = dayjs.duration(Math.round(raw * 1000));
const fmt = [];
if (duration.get("day")) fmt.push("D[d]");
if (duration.get("hour")) fmt.push("H[h]");
if (duration.get("minute")) fmt.push("m[m]");
if (duration.get("millisecond")) {
fmt.push("s.SSS[s]");
} else if (!fmt.length || duration.get("second")) {
fmt.push("s[s]");
}
return duration.format(fmt.join(" "));
};

View File

@@ -1,11 +1,9 @@
import { FC, useState, useEffect } from "preact/compat";
import { JSX } from "preact";
import { ArrowDownIcon } from "../Icons";
import "./style.scss";
import { ReactNode } from "react";
interface AccordionProps {
id?: string
title: ReactNode
children: ReactNode
defaultExpanded?: boolean
@@ -16,24 +14,21 @@ const Accordion: FC<AccordionProps> = ({
defaultExpanded = false,
onChange,
title,
children,
id,
children
}) => {
const [isOpen, setIsOpen] = useState(defaultExpanded);
const toggleOpen = (event: JSX.TargetedMouseEvent<HTMLElement>) => {
const toggleOpen = () => {
const selection = window.getSelection();
if ((event.target as HTMLElement).closest("button")) {
event.preventDefault();
return; // If the text is selected, cancel the execution of toggle.
}
if (selection && selection.toString()) {
event.preventDefault();
return; // If the text is selected, cancel the execution of toggle.
}
const details = event.currentTarget.parentElement as HTMLDetailsElement;
onChange && onChange(details.open);
setIsOpen(details.open);
setIsOpen((prev) => {
const newState = !prev;
onChange && onChange(newState);
return newState;
});
};
useEffect(() => {
@@ -42,23 +37,23 @@ const Accordion: FC<AccordionProps> = ({
return (
<>
<details
className="vm-accordion-section"
key="content"
open={isOpen}
id={id}
<header
className={`vm-accordion-header ${isOpen && "vm-accordion-header_open"}`}
onClick={toggleOpen}
>
<summary
className="vm-accordion-header"
onClick={toggleOpen}
{title}
<div className={`vm-accordion-header__arrow ${isOpen && "vm-accordion-header__arrow_open"}`}>
<ArrowDownIcon />
</div>
</header>
{isOpen && (
<section
className="vm-accordion-section"
key="content"
>
{title}
<div className="vm-accordion-header__arrow">
<ArrowDownIcon />
</div>
</summary>
{children}
</details>
{children}
</section>
)}
</>
);
};

View File

@@ -17,6 +17,10 @@
transform: rotate(0);
transition: transform 200ms ease-in-out;
&_open {
transform: rotate(180deg);
}
svg {
width: 14px;
height: auto;
@@ -24,14 +28,6 @@
}
}
.vm-accordion-section[open] > summary {
& > .vm-accordion-header {
&__arrow {
transform: rotate(180deg);
}
}
}
.accordion-section {
overflow: hidden;
}

View File

@@ -1,27 +1,41 @@
@use "src/styles/variables" as *;
.vm-alert {
z-index: 20;
position: sticky;
top: $padding-global;
position: relative;
display: grid;
grid-template-columns: 20px 1fr;
align-items: center;
gap: $padding-small;
padding: $padding-global;
background-color: $color-background-block;
border-radius: $border-radius-medium;
box-shadow: $box-shadow;
font-size: $font-size;
font-weight: normal;
color: $color-text;
line-height: 1.5;
opacity: 0.8;
&_mobile {
align-items: flex-start;
border-radius: 0;
}
&:after {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: $border-radius-medium;
z-index: 1;
opacity: 0.1;
}
&_mobile:after {
border-radius: 0;
}
&__icon,
&__content {
position: relative;
@@ -34,53 +48,54 @@
justify-content: center;
align-self: flex-start;
min-height: 24px;
filter: brightness(0.6);
}
&__content {
filter: brightness(0.6);
white-space: pre-line;
text-wrap: balance;
overflow-wrap: anywhere;
filter: brightness(0.6);
}
&_success {
color: $color-success;
background-color: $color-background-success;
&:after {
background-color: $color-success;
}
}
&_error {
color: $color-error;
background-color: $color-background-error;
&:after {
background-color: $color-error;
}
}
&_info {
color: $color-info;
background-color: $color-background-info;
&:after {
background-color: $color-info;
}
}
&_warning {
color: $color-warning;
background-color: $color-background-warning;
&:after {
background-color: $color-warning;
}
}
&_dark &__content, &_dark &__icon {
&_dark {
&:after {
opacity: 0.1;
}
}
&_dark &__content {
filter: none;
}
&_dark:is(&_success) {
border: 0.5px solid $color-success;
}
&_dark:is(&_error) {
border: 0.5px solid $color-error;
}
&_dark:is(&_info) {
border: 0.5px solid $color-info;
}
&_dark:is(&_warning) {
border: 0.5px solid $color-warning;
}
}

View File

@@ -15,7 +15,6 @@ export interface AutocompleteOptions {
}
interface AutocompleteProps {
itemClassName?: string
value: string
options: AutocompleteOptions[]
anchor: React.RefObject<HTMLElement>
@@ -42,7 +41,6 @@ enum FocusType {
const Autocomplete: FC<AutocompleteProps> = ({
value,
itemClassName,
options,
anchor,
disabled,
@@ -214,9 +212,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
>
{selected?.includes(option.value) && <DoneIcon/>}
<>{option.icon}</>
<div className={`vm-list-item-inner ${itemClassName} ${option.value.toLowerCase().replace(" ", "-")}`}>
<span>{option.value}</span>
</div>
<span>{option.value}</span>
</div>
)}
</div>

View File

@@ -1,61 +1,5 @@
import { getCssVariable } from "../../../utils/theme";
export const LinkIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0m2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z"/>
</svg>
);
export const GroupIcon = () => (
<svg
fill="currentColor"
viewBox="0 0 512 512"
>
<path d="M170.667 64v42.667h-64v298.666h64V448H64V64zM448 64v384H341.333v-42.667h64V106.667h-64V64zm-85.333 256v42.667H149.333V320zm0-85.333v42.666H149.333v-42.666zm0-85.334V192H149.333v-42.667z"/>
</svg>
);
export const DetailsIcon = () => (
<svg
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M12 3a2 2 0 1 0-4 0 2 2 0 0 0 4 0m-2 5a2 2 0 1 1 0 4 2 2 0 0 1 0-4m0 7a2 2 0 1 1 0 4 2 2 0 0 1 0-4"/>
</svg>
);
export const AlertIcon = () => (
<svg
viewBox="-1 0 30 30"
fill="currentColor"
>
<path d="m3 24 3-6v-8a8 8 0 0 1 16 0v8l3 6zm11 4a2.99 2.99 0 0 1-2.816-2h5.632A2.99 2.99 0 0 1 14 28m10-10v-8c0-5.522-4.478-10-10-10S4 4.478 4 10v8l-4 8h9.101a5 5 0 0 0 9.798 0H28z"/>
</svg>
);
export const AlertingRuleIcon = () => (
<svg
viewBox="411.014 448.582 21.637 17.836"
fill="currentColor"
>
<path d="M411.47 453.837a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m-.016 5.54c-.284.124-.44.372-.439.701.001.354.204.614.553.708.092.025 1.214.034 3.267.028 3.481-.011 3.23.008 3.468-.264a.714.714 0 0 0-.003-.963c-.249-.283-.03-.267-3.564-.266-2.735 0-3.171.008-3.282.056m-.018 5.016a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m.161-15.761a.85.85 0 0 0-.389.417c-.023.07-.034.224-.026.343.018.239.078.345.301.525l.139.112 8.729.01 8.73.009.163-.079c.193-.093.36-.332.395-.565a.75.75 0 0 0-.388-.75c-.142-.072-.227-.073-8.844-.072-7.105.002-8.721.011-8.81.05m14.005 2.595c-.286.18-.371.401-.371.961v.334l-.499.024c-.598.028-.961.126-1.456.392a3.5 3.5 0 0 0-1.721 2.199c-.081.307-.091.479-.115 1.923-.027 1.566-.028 1.59-.138 1.966-.145.496-.557 1.361-.929 1.945a5 5 0 0 0-.368.677c-.1.292-.095.679.013.982.112.32.461.686.75.789.276.099 1.255.259 2.268.373l.84.095.028.287q.089.935.767 1.579a2.383 2.383 0 0 0 3.659-.434c.227-.351.36-.745.396-1.161l.023-.283.291-.027c.956-.093 2.47-.32 2.715-.408.393-.14.694-.464.817-.875.16-.539.093-.833-.354-1.554-.373-.601-.832-1.565-.956-2.007-.083-.29-.093-.448-.119-1.903-.027-1.427-.039-1.619-.118-1.924a3.5 3.5 0 0 0-1.895-2.327c-.422-.202-.758-.282-1.309-.312l-.489-.025-.022-.473c-.022-.521-.062-.621-.325-.806-.124-.088-.182-.096-.69-.096-.489.001-.57.012-.693.089m2.696 2.786c.546.176.994.583 1.249 1.135l.149.326.025 1.543c.027 1.672.046 1.837.286 2.598.166.52.621 1.468.974 2.028.189.303.274.472.244.492-.104.066-1.778.288-2.915.387-.788.068-3.246.068-4.037 0-1.154-.099-2.811-.32-2.919-.39-.035-.023.03-.16.223-.469.375-.603.805-1.493.976-2.024.246-.763.272-1 .272-2.426 0-.701.019-1.398.043-1.549.083-.554.47-1.148.931-1.429.103-.063.308-.157.453-.209.258-.092.315-.094 2.025-.096 1.642-.001 1.776.005 2.021.083m-1.384 10.771a1.06 1.06 0 0 1-.748.2c-.394-.066-.776-.451-.835-.841l-.026-.168h2.005v.108z"/>
</svg>
);
export const RecordingRuleIcon = () => (
<svg
viewBox="411.014 448.582 23.358 18.492"
fill="currentColor"
>
<path d="M411.47 453.837a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m15.967 7.103a1.592 1.612 0 1 1 1.592-1.612 1.592 1.612 0 0 1-1.592 1.612m0-1.612"/>
<path d="M427.405 466.377a6.966 7.052 0 1 1 6.965-7.053 6.974 7.06 0 0 1-6.965 7.053m0-12.09a4.975 5.037 0 1 0 4.975 5.037 4.981 5.043 0 0 0-4.975-5.037"/>
<path d="M421.832 467.074a.996 1.008 0 0 1-.708-1.715l3.582-3.675a.995 1.008 0 0 1 1.417 1.415l-3.582 3.675a.995 1.007 0 0 1-.709.3m-10.378-7.697c-.284.124-.44.372-.439.701.001.354.204.614.553.708.092.025 1.214.034 3.267.028 3.481-.011 3.23.008 3.468-.264a.714.714 0 0 0-.003-.963c-.249-.283-.03-.267-3.564-.266-2.735 0-3.171.008-3.282.056m-.018 5.016a.72.72 0 0 0-.422.679c0 .319.137.535.422.667.139.065.322.068 3.814.069l3.668.001.164-.083c.484-.245.535-.904.096-1.254l-.141-.112-3.746-.008c-3.05-.006-3.766.001-3.855.041m.161-15.761a.85.85 0 0 0-.389.417c-.023.07-.034.224-.026.343.018.239.078.345.301.525l.139.112 8.729.01 8.73.009.163-.079c.193-.093.36-.332.395-.565a.75.75 0 0 0-.388-.75c-.142-.072-.227-.073-8.844-.072-7.105.002-8.721.011-8.81.05"/>
</svg>
);
export const LogoIcon = () => (
<svg
viewBox="0 0 74 24"

View File

@@ -1,5 +1,4 @@
import { FC, useCallback, useEffect, createPortal } from "preact/compat";
import { JSX } from "preact/jsx-runtime";
import { CloseIcon } from "../Icons";
import Button from "../Button/Button";
import { ReactNode, MouseEvent } from "react";
@@ -10,7 +9,7 @@ import { useLocation, useNavigate } from "react-router-dom";
import useEventListener from "../../../hooks/useEventListener";
interface ModalProps {
title: JSX.Element | string
title?: string
children: ReactNode
onClose: () => void
className?: string

View File

@@ -5,11 +5,10 @@ import { MouseEvent } from "react";
interface MultipleSelectedValueProps {
values: string[]
itemClassName?: string
onRemoveItem: (val: string) => void
}
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, itemClassName, onRemoveItem }) => {
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemoveItem }) => {
const { isMobile } = useDeviceDetect();
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
@@ -28,7 +27,7 @@ const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, itemCla
return <>
{values.map(item => (
<div
className={`vm-select-input-content__selected ${itemClassName} ${item.toLowerCase().replace(" ", "-")}`}
className="vm-select-input-content__selected"
key={item}
>
<span>{item}</span>

View File

@@ -11,7 +11,6 @@ import useEventListener from "../../../hooks/useEventListener";
import useClickOutside from "../../../hooks/useClickOutside";
interface SelectProps {
itemClassName?: string
value: string | string[]
list: string[]
label?: string
@@ -21,7 +20,6 @@ interface SelectProps {
searchable?: boolean
autofocus?: boolean
disabled?: boolean
includeAll?: boolean
onChange: (value: string) => void
}
@@ -29,14 +27,12 @@ const Select: FC<SelectProps> = ({
value,
list,
label,
itemClassName,
placeholder,
noOptionsText,
clearable = false,
searchable = false,
autofocus,
disabled,
includeAll,
onChange
}) => {
const { isDarkTheme } = useAppState();
@@ -50,7 +46,7 @@ const Select: FC<SelectProps> = ({
const inputRef = useRef<HTMLInputElement>(null);
const isMultiple = Array.isArray(value);
const selectedValues = Array.isArray(value) ? value.slice() : [];
const selectedValues = Array.isArray(value) ? value : undefined;
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
const textFieldValue = useMemo(() => {
@@ -123,9 +119,6 @@ const Select: FC<SelectProps> = ({
useEventListener("keyup", handleKeyUp);
useClickOutside(autocompleteAnchorEl, handleCloseList, wrapperRef);
includeAll && !list.includes("All") && list.push("All");
includeAll && !selectedValues?.length && selectedValues.push("All");
return (
<div
className={classNames({
@@ -142,12 +135,11 @@ const Select: FC<SelectProps> = ({
<div className="vm-select-input-content">
{!!selectedValues?.length && (
<MultipleSelectedValue
itemClassName={itemClassName}
values={selectedValues}
onRemoveItem={handleSelected}
/>
)}
{!hideInput && !selectedValues?.length && (
{!hideInput && (
<input
value={textFieldValue}
type="text"
@@ -179,10 +171,9 @@ const Select: FC<SelectProps> = ({
</div>
</div>
<Autocomplete
itemClassName={itemClassName}
label={label}
value={autocompleteValue}
options={list.map(l => ({ value: l }))}
options={list.map(el => ({ value: el }))}
anchor={autocompleteAnchorEl}
selected={selectedValues}
minLength={1}

View File

@@ -1,17 +0,0 @@
import { RuleType } from "../types";
export const RULE_TYPES: RuleType[] = [
{
id: "alerts",
title: "Alerts",
isDefault: true,
},
{
title: "Recording",
id: "recording",
},
{
title: "All",
id: "all",
},
];

View File

@@ -6,3 +6,5 @@ export enum AppType {
export const APP_TYPE = import.meta.env.VITE_APP_TYPE;
export const APP_TYPE_VM = APP_TYPE === AppType.victoriametrics;
export const APP_TYPE_ANOMALY = APP_TYPE === AppType.vmanomaly;

View File

@@ -1,21 +1,13 @@
export const darkPalette = {
"color-primary": "#589df6",
"color-primary": "#589DF6",
"color-secondary": "#316eca",
"color-error": "#e5534b",
"color-background-error": "#240705",
"color-warning": "#c69026",
"color-background-warning": "#221906",
"color-info": "#539bf5",
"color-background-info": "#021327",
"color-success": "#57ab5a",
"color-background-success": "#0e1b0e",
"color-passive": "#a7acb3",
"color-background-body": "#22272e",
"color-background-block": "#2d333b",
"color-background-tooltip": "rgba(22, 22, 22, 0.8)",
"color-background-item": "#313944",
"color-background-badge": "#4e5a6a",
"color-background-hover": "#3D4652",
"color-text": "#cdd9e5",
"color-text-secondary": "#768390",
"color-text-disabled": "#636e7b",
@@ -33,23 +25,15 @@ export const darkPalette = {
};
export const lightPalette = {
"color-primary": "#3f51b5",
"color-secondary": "#e91e63",
"color-error": "#fd080e",
"color-background-error": "#ffd7d8",
"color-warning": "#ff8308",
"color-background-warning": "#ffd6ad",
"color-info": "#03a9f4",
"color-background-info": "#d7f2fe",
"color-success": "#4caf50",
"color-background-success": "#d4ecd5",
"color-passive": "#5d6267",
"color-primary": "#3F51B5",
"color-secondary": "#E91E63",
"color-error": "#FD080E",
"color-warning": "#FF8308",
"color-info": "#03A9F4",
"color-success": "#4CAF50",
"color-background-body": "#FEFEFF",
"color-background-block": "#FFFFFF",
"color-background-tooltip": "rgba(80,80,80,0.9)",
"color-background-item": "#f8f9fa",
"color-background-badge": "#e1e4e7",
"color-background-hover": "#edf0f2",
"color-text": "#110f0f",
"color-text-secondary": "#706F6F",
"color-text-disabled": "#A09F9F",

View File

@@ -3,7 +3,7 @@ import { useEffect, useState } from "preact/compat";
import { ErrorTypes } from "../types";
import { APP_TYPE_VM } from "../constants/appType";
const useFetchAppConfig = () => {
const useFetchFlags = () => {
const dispatch = useAppDispatch();
const [isLoading, setIsLoading] = useState(false);
@@ -31,5 +31,5 @@ const useFetchAppConfig = () => {
return { isLoading, error };
};
export default useFetchAppConfig;
export default useFetchFlags;

View File

@@ -0,0 +1,45 @@
import { useAppDispatch, useAppState } from "../state/common/StateContext";
import { useEffect, useState } from "preact/compat";
import { ErrorTypes } from "../types";
import { APP_TYPE_VM } from "../constants/appType";
import { getUrlWithoutTenant } from "../utils/tenants";
const useFetchFlags = () => {
const { serverUrl } = useAppState();
const dispatch = useAppDispatch();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>("");
useEffect(() => {
const fetchFlags = async () => {
if (!serverUrl || !APP_TYPE_VM) return;
setError("");
setIsLoading(true);
try {
const url = getUrlWithoutTenant(serverUrl).replace(/\/prometheus\/?$/, "");
const response = await fetch(`${url}/flags`);
const data = await response.text();
const flags = data.split("\n").filter(flag => flag.trim() !== "")
.reduce((acc, flag) => {
const [keyRaw, valueRaw] = flag.split("=");
const key = keyRaw.trim().replace(/^-/, "");
acc[key.trim()] = valueRaw ? valueRaw.trim().replace(/^"(.*)"$/, "$1") : null;
return acc;
}, {} as Record<string, string|null>);
dispatch({ type: "SET_FLAGS", payload: flags });
} catch (e) {
setIsLoading(false);
if (e instanceof Error) setError(`${e.name}: ${e.message}`);
}
};
fetchFlags();
}, [serverUrl]);
return { isLoading, error };
};
export default useFetchFlags;

View File

@@ -7,20 +7,12 @@ const useSearchParamsFromObject = () => {
const [searchParams, setSearchParams] = useSearchParams();
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, string | number>) => {
const hasSearchParams = !!searchParams.size;
const hasSearchParams = !!Array.from(searchParams.values()).length;
let hasChanged = false;
const newSearchParams = new URLSearchParams(searchParams);
searchParams.keys().forEach(key => {
if (!(key in objectParams)) {
newSearchParams.delete(key);
hasChanged = true;
}
});
Object.entries(objectParams).forEach(([key, value]) => {
if (newSearchParams.get(key) !== `${value}`) {
newSearchParams.set(key, `${value}`);
if (searchParams.get(key) !== `${value}`) {
searchParams.set(key, `${value}`);
hasChanged = true;
}
});
@@ -28,9 +20,9 @@ const useSearchParamsFromObject = () => {
if (!hasChanged) return;
if (hasSearchParams) {
setSearchParams(newSearchParams);
setSearchParams(searchParams);
} else {
navigate(`?${newSearchParams.toString()}`, { replace: true });
navigate(`?${searchParams.toString()}`, { replace: true });
}
}, [searchParams, navigate]);

View File

@@ -1,11 +1,12 @@
import Header from "../Header/Header";
import { FC, useEffect } from "preact/compat";
import { Outlet, useSearchParams } from "react-router-dom";
import { Outlet, useLocation, useSearchParams } from "react-router-dom";
import qs from "qs";
import "../MainLayout/style.scss";
import { getAppModeEnable } from "../../utils/app-mode";
import classNames from "classnames";
import Footer from "../Footer/Footer";
import { routerOptions } from "../../router";
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
@@ -13,10 +14,17 @@ import ControlsAnomalyLayout from "./ControlsAnomalyLayout";
const AnomalyLayout: FC = () => {
const appModeEnable = getAppModeEnable();
const { isMobile } = useDeviceDetect();
const { pathname } = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
useFetchDefaultTimezone();
const setDocumentTitle = () => {
const defaultTitle = "vmui for vmanomaly";
const routeTitle = routerOptions[pathname]?.title;
document.title = routeTitle ? `${routeTitle} - ${defaultTitle}` : defaultTitle;
};
// for support old links with search params
const redirectSearchToHashParams = () => {
const { search, href } = window.location;
@@ -30,6 +38,7 @@ const AnomalyLayout: FC = () => {
if (newHref !== href) window.location.replace(newHref);
};
useEffect(setDocumentTitle, [pathname]);
useEffect(redirectSearchToHashParams, []);
return <section className="vm-container">

View File

@@ -14,8 +14,7 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
displaySidebar,
isMobile,
headerSetup,
accountIds,
closeModal,
accountIds
}) => {
return (
@@ -29,11 +28,7 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
{headerSetup?.executionControls && <ExecutionControls
tooltip={headerSetup?.executionControls?.tooltip}
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
closeModal={closeModal}
/>}
{headerSetup?.executionControls && <ExecutionControls/>}
<GlobalSettings/>
{!displaySidebar && <ShortcutKeys/>}
</div>

View File

@@ -105,7 +105,6 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
controlsComponent={controlsComponent}
displaySidebar={displaySidebar}
isMobile={isMobile}
closeModal={() => {}}
/>
</header>;
};

View File

@@ -18,7 +18,6 @@ export interface ControlsProps {
isMobile?: boolean;
headerSetup?: RouterOptionsHeader;
accountIds?: string[];
closeModal: () => void;
}
const HeaderControls: FC<ControlsProps & HeaderProps> = ({
@@ -46,7 +45,6 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
isMobile={isMobile}
accountIds={accountIds}
headerSetup={headerSetup}
closeModal={handleCloseList}
/>
);

View File

@@ -14,8 +14,7 @@ const ControlsMainLayout: FC<ControlsProps> = ({
displaySidebar,
isMobile,
headerSetup,
accountIds,
closeModal,
accountIds
}) => {
return (
@@ -29,11 +28,7 @@ const ControlsMainLayout: FC<ControlsProps> = ({
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
{headerSetup?.executionControls && <ExecutionControls
tooltip={headerSetup?.executionControls?.tooltip}
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
closeModal={closeModal}
/>}
{headerSetup?.executionControls && <ExecutionControls/>}
<GlobalSettings/>
{!displaySidebar && <ShortcutKeys/>}
</div>

View File

@@ -11,6 +11,7 @@ import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchD
import useDeviceDetect from "../../hooks/useDeviceDetect";
import ControlsMainLayout from "./ControlsMainLayout";
import useFetchDefaultTimezone from "../../hooks/useFetchDefaultTimezone";
import useFetchFlags from "../../hooks/useFetchFlags";
import useFetchAppConfig from "../../hooks/useFetchAppConfig";
const MainLayout: FC = () => {
@@ -22,6 +23,7 @@ const MainLayout: FC = () => {
useFetchDashboards();
useFetchDefaultTimezone();
useFetchAppConfig();
useFetchFlags();
const setDocumentTitle = () => {
const defaultTitle = "vmui";

View File

@@ -1,62 +0,0 @@
import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert";
import { useFetchItem } from "./hooks/useFetchItem";
import "./style.scss";
import { Alert as APIAlert } from "../../types";
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
import BaseAlert from "../../components/ExploreAlerts/BaseAlert";
import Modal from "../../components/Main/Modal/Modal";
interface ExploreAlertProps {
groupId: string;
id: string;
mode: string;
onClose: () => void;
}
const ExploreAlert = ({ groupId, id, mode, onClose }: ExploreAlertProps) => {
const {
item,
isLoading,
error,
} = useFetchItem<APIAlert>({ groupId, id, mode });
if (isLoading) return (
<Spinner />
);
if (error) return (
<Alert variant="error">{error}</Alert>
);
const noItemFound = `No alert with group ID=${groupId}, alert ID=${id} found!`;
const states = {
firing: 1,
};
return (
<Modal
className="vm-explore-alerts"
title={item ? (
<ItemHeader
entity="alert"
type="alerting"
groupId={item.group_id}
id={item.id}
name={item.name}
states={states}
onClose={onClose}
/>
) : "Alert not found"}
onClose={onClose}
>
<div className="vm-explore-alerts">
{item && (<BaseAlert item={item} />) || (
<Alert variant="info">{noItemFound}</Alert>
)}
</div>
</Modal>
);
};
export default ExploreAlert;

View File

@@ -1,55 +0,0 @@
import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert";
import { useFetchGroup } from "./hooks/useFetchGroup";
import "./style.scss";
import { Group as APIGroup } from "../../types";
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
import BaseGroup from "../../components/ExploreAlerts/BaseGroup";
import Modal from "../../components/Main/Modal/Modal";
interface ExploreGroupProps {
id: string;
onClose: () => void;
}
const ExploreGroup = ({ id, onClose }: ExploreGroupProps) => {
const {
group,
isLoading,
error,
} = useFetchGroup<APIGroup>({ id });
if (isLoading) return (
<Spinner />
);
if (error) return (
<Alert variant="error">{error}</Alert>
);
const noGroupFound = `No group ID=${id} found!`;
return (
<Modal
className="vm-explore-alerts"
title={group ? (
<ItemHeader
entity="group"
groupId={id}
name={group.name}
states={group.states}
onClose={onClose}
/>
) : "Rule not found"}
onClose={onClose}
>
<div className="vm-explore-alerts">
{group && (<BaseGroup group={group} />) || (
<Alert variant="info">{noGroupFound}</Alert>
)}
</div>
</Modal>
);
};
export default ExploreGroup;

View File

@@ -1,142 +0,0 @@
import { FC, useEffect, useState } from "preact/compat";
import { useLocation } from "react-router";
import { useNotifiersSetQueryParams as useSetQueryParams } from "./hooks/useSetQueryParams";
import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert";
import Accordion from "../../components/Main/Accordion/Accordion";
import { useFetchNotifiers } from "./hooks/useFetchNotifiers";
import "./style.scss";
import NotifiersHeader from "../../components/ExploreAlerts/NotifiersHeader";
import NotifierHeader from "../../components/ExploreAlerts/NotifierHeader";
import Target from "../../components/ExploreAlerts/Target";
import { Notifier as APINotifier, Target as APITarget } from "../../types";
import { getQueryStringValue } from "../../utils/query-string";
import { getChanges } from "./helpers";
import debounce from "lodash.debounce";
const defaultKindsStr = getQueryStringValue("kinds", "") as string;
const defaultKinds = defaultKindsStr.split("&").filter((rt) => rt) as string[];
const defaultSearchInput = getQueryStringValue("search", "") as string;
const ExploreNotifiers: FC = () => {
const {
notifiers,
isLoading,
error,
} = useFetchNotifiers();
const [searchInput, setSearchInput] = useState(defaultSearchInput);
const [kinds, setKinds] = useState(defaultKinds);
useSetQueryParams({
kinds: kinds.join("&"),
search: searchInput,
});
const location = useLocation();
const pageLoaded = !isLoading && !error && !!notifiers?.length;
const savedScrollTop = localStorage.getItem("scrollTop");
useEffect(() => {
if (!pageLoaded) return;
if (location.hash) {
const target = document.querySelector(location.hash);
if (target) {
let parent = target.closest("details");
while (parent) {
parent.open = true;
if (!parent?.parentElement) return;
parent = parent.parentElement.closest("details");
}
target.scrollIntoView();
}
} else {
if (savedScrollTop) {
window.scrollTo(0, parseInt(savedScrollTop));
}
const handleBeforeUnload = () => {
localStorage.setItem("scrollTop", (window.scrollY || 0).toString());
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}
}, [location, savedScrollTop, pageLoaded]);
const handleChangeSearch = (input: string) => {
if (!input) {
setSearchInput("");
} else {
setSearchInput(input);
}
};
const allKinds: Set<string> = new Set();
const filteredNotifiers: APINotifier[] = [];
notifiers.forEach((notifier) => {
const filteredTargets: APITarget[] = [];
const targets = notifier.targets || [];
targets.forEach((target) => {
allKinds.add(notifier.kind);
if (kinds?.length && !kinds.includes(notifier.kind)) return;
if (
searchInput &&
!target.address.toLowerCase().includes(searchInput.toLowerCase()) &&
!notifier.kind.toLowerCase().includes(searchInput.toLowerCase())
)
return;
filteredTargets.push(target);
});
if (filteredTargets.length) {
const n = Object.assign({}, notifier);
n.targets = filteredTargets;
filteredNotifiers.push(n);
}
});
const handleChangeKinds = (title: string) => {
setKinds(getChanges(title, kinds));
};
return (
<div className="vm-explore-alerts">
<NotifiersHeader
kinds={kinds}
allKinds={Array.from(allKinds)}
onChangeKinds={handleChangeKinds}
onChangeSearch={debounce(handleChangeSearch, 500)}
/>
{(isLoading && <Spinner />) || (error && <Alert variant="error">{error}</Alert>) || (
!filteredNotifiers.length && <Alert variant="info">No notifiers found!</Alert>
) || (
<div className="vm-explore-alerts-body">
{filteredNotifiers.map((notifier) => (
<div
key={notifier.kind}
className="vm-explore-alert-group vm-block vm-block_empty-padding"
>
<Accordion
key={`notifier-${notifier.kind}`}
id={`notifier-${notifier.kind}`}
title={<NotifierHeader notifier={notifier} />}
>
<div className="vm-explore-alerts-items">
{notifier.targets.map((target) => (
<Target
key={`target-${target.address}`}
target={target}
/>
))}
</div>
</Accordion>
</div>
))}
</div>
)}
</div>
);
};
export default ExploreNotifiers;

View File

@@ -1,60 +0,0 @@
import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert";
import { useFetchItem } from "./hooks/useFetchItem";
import "./style.scss";
import { Rule as APIRule } from "../../types";
import ItemHeader from "../../components/ExploreAlerts/ItemHeader";
import BaseRule from "../../components/ExploreAlerts/BaseRule";
import Modal from "../../components/Main/Modal/Modal";
import { getStates } from "./helpers";
interface ExploreRuleProps {
groupId: string;
id: string;
mode: string;
onClose: () => void;
}
const ExploreRule = ({ groupId, id, mode, onClose }: ExploreRuleProps) => {
const {
item,
isLoading,
error,
} = useFetchItem<APIRule>({ groupId, id, mode });
if (isLoading) return (
<Spinner />
);
if (error) return (
<Alert variant="error">{error}</Alert>
);
const noItemFound = `No rule with group ID=${groupId}, rule ID=${id} found!`;
return (
<Modal
className="vm-explore-alerts"
title={item ? (
<ItemHeader
entity="rule"
type={item.type}
groupId={item.group_id}
states={getStates(item)}
id={item.id}
name={item.name}
onClose={onClose}
/>
) : "Rule not found"}
onClose={onClose}
>
<div className="vm-explore-alerts">
{item && (<BaseRule item={item} />) || (
<Alert variant="info">{noItemFound}</Alert>
)}
</div>
</Modal>
);
};
export default ExploreRule;

View File

@@ -1,206 +0,0 @@
import { FC, useEffect, useMemo, useState, useCallback } from "preact/compat";
import { useNavigate, useLocation, useSearchParams } from "react-router";
import { useRulesSetQueryParams as useSetQueryParams } from "./hooks/useSetQueryParams";
import Spinner from "../../components/Main/Spinner/Spinner";
import Alert from "../../components/Main/Alert/Alert";
import Accordion from "../../components/Main/Accordion/Accordion";
import { useFetchGroups } from "./hooks/useFetchGroups";
import "./style.scss";
import RulesHeader from "../../components/ExploreAlerts/RulesHeader";
import GroupHeader from "../../components/ExploreAlerts/GroupHeader";
import Rule from "../../components/ExploreAlerts/Rule";
import ExploreRule from "../../pages/ExploreAlerts/ExploreRule";
import ExploreAlert from "../../pages/ExploreAlerts/ExploreAlert";
import ExploreGroup from "../../pages/ExploreAlerts/ExploreGroup";
import { getQueryStringValue } from "../../utils/query-string";
import { getStates, getChanges, filterGroups } from "./helpers";
import debounce from "lodash.debounce";
const defaultTypesStr = getQueryStringValue("types", "") as string;
const defaultTypes = defaultTypesStr.split("&").filter((rt) => rt) as string[];
const defaultStatesStr = getQueryStringValue("states", "") as string;
const defaultStates = defaultStatesStr.split("&").filter((s) => s) as string[];
const defaultSearchInput = getQueryStringValue("search", "") as string;
const ExploreRules: FC = () => {
const groupId = getQueryStringValue("group_id", "") as string;
const ruleId = getQueryStringValue("rule_id", "") as string;
const alertId = getQueryStringValue("alert_id", "") as string;
const [searchInput, setSearchInput] = useState(defaultSearchInput);
const [types, setTypes] = useState(defaultTypes);
const [states, setStates] = useState(defaultStates);
const [modalOpen, setModalOpen] = useState(true);
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
if (!location.hash && groupId) {
setModalOpen(true);
} else {
setModalOpen(false);
}
}, [location.hash, groupId]);
useSetQueryParams({
types: types.join("&"),
states: states.join("&"),
search: searchInput,
group_id: groupId,
alert_id: alertId,
rule_id: ruleId,
});
const handleChangeSearch = useCallback((input: string) => {
if (!input) {
setSearchInput("");
} else {
setSearchInput(input);
}
}, [searchInput]);
const getModal = () => {
if (ruleId !== "") {
return (
<ExploreRule
groupId={groupId}
id={ruleId}
mode={ruleId !== "" ? "rule" : "alert"}
onClose={handleClose(`rule-${ruleId}`)}
/>
);
} else if (alertId !== "") {
return (
<ExploreAlert
groupId={groupId}
id={alertId}
mode={ruleId !== "" ? "rule" : "alert"}
onClose={handleClose(`alert-${alertId}`)}
/>
);
} else if (groupId !== "") {
return (
<ExploreGroup
id={groupId}
onClose={handleClose(`group-${groupId}`)}
/>
);
}
};
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) => {
return () => {
const newParams = new URLSearchParams(searchParams);
newParams.delete("group_id");
newParams.delete("rule_id");
newParams.delete("alert_id");
setSearchParams(newParams);
setModalOpen(false);
navigate({
hash: `#${id}`,
});
};
};
const {
groups,
isLoading,
error,
} = useFetchGroups({ blockFetch: modalOpen });
const pageLoaded = !isLoading && !error && !!groups?.length;
const savedScrollTop = localStorage.getItem("scrollTop");
useEffect(() => {
if (!pageLoaded) return;
if (location.hash) {
const target = document.querySelector(location.hash);
if (target) {
let parent = target.closest("details");
while (parent) {
parent.open = true;
if (!parent?.parentElement) return;
parent = parent.parentElement.closest("details");
}
target.scrollIntoView();
}
} else {
if (savedScrollTop) {
window.scrollTo(0, parseInt(savedScrollTop));
}
const updateScrollPosition = () => {
localStorage.setItem("scrollTop", (window.scrollY || 0).toString());
};
window.addEventListener("scroll", updateScrollPosition);
return () => {
window.removeEventListener("scroll", updateScrollPosition);
};
}
}, [location, savedScrollTop, pageLoaded]);
const { filteredGroups, allTypes, allStates } = useMemo(
() => filterGroups(groups || [], types, states, searchInput),
[groups, types, states, searchInput]
);
return (
<>
{modalOpen && getModal()}
{(!modalOpen || !!allStates?.size) && (
<div className="vm-explore-alerts">
<RulesHeader
types={types}
allTypes={Array.from(allTypes)}
states={states}
allStates={Array.from(allStates)}
onChangeTypes={handleChangeTypes}
onChangeStates={handleChangeStates}
onChangeSearch={debounce(handleChangeSearch, 500)}
/>
{(isLoading && <Spinner />) || (error && <Alert variant="error">{error}</Alert>) || (
!filteredGroups.length && <Alert variant="info">{noRuleFound}</Alert>
) || (
<div className="vm-explore-alerts-body">
{filteredGroups.map((group) => (
<div
key={group.id}
className="vm-explore-alert-group vm-block vm-block_empty-padding"
>
<Accordion
key={`group-${group.id}`}
id={`group-${group.id}`}
title={<GroupHeader group={group} />}
>
<div className="vm-explore-alerts-items">
{group.rules.map((rule) => (
<Rule
key={`rule-${rule.id}`}
rule={rule}
states={getStates(rule)}
/>
))}
</div>
</Accordion>
</div>
))}
</div>
)}
</div>
)}
</>
);
};
export default ExploreRules;

View File

@@ -1,88 +0,0 @@
import { Rule, Group } from "../../types";
export const getChanges = (title: string, prevValues: string[]): string[] => {
if (title === "All") return [];
const newValues = new Set<string>(prevValues);
if (newValues.has(title)) {
newValues.delete(title);
} else {
newValues.add(title);
}
return Array.from(newValues);
};
export const getState = (rule: Rule) => {
let state = rule?.state || "ok";
if (rule?.health !== "ok") {
state = "unhealthy";
} else if (!rule?.lastSamples && !rule?.lastSeriesFetched) {
state = "no match";
}
return state;
};
export const getStates = (rule: Rule) => {
const output: Record<string, number> = {};
const alertsCount = rule?.alerts?.length || 0;
if (alertsCount > 0) {
rule.alerts.forEach((alert) => {
if (alert.state in output) {
output[alert.state] += 1;
} else {
output[alert.state] = 1;
}
});
} else {
output[getState(rule)] = 1;
}
return output;
};
export const filterGroups = (groups: Group[], types: string[], states: string[], searchInput: string) => {
const allTypes: Set<string> = new Set();
const allStates: Set<string> = new Set();
const filteredGroups: Group[] = [];
groups.forEach((group) => {
const filteredRules: Rule[] = [];
const statesPerGroup: Record<string, number> = {};
group.rules.forEach((rule) => {
const ruleType = rule.type.charAt(0).toUpperCase() + rule.type.slice(1);
allTypes.add(ruleType);
if (types?.length && !types.includes(ruleType)) return;
const state = getState(rule);
const stateName = state.charAt(0).toUpperCase() + state.slice(1);
allStates.add(stateName);
if (states?.length && !states.includes(stateName)) return;
if (
searchInput &&
!rule.name.toLowerCase().includes(searchInput.toLowerCase()) &&
!group.name.toLowerCase().includes(searchInput.toLowerCase()) &&
!group.file.toLowerCase().includes(searchInput.toLowerCase())
)
return;
filteredRules.push(rule);
if (state !== "no match" && state !== "unhealthy" && state !== "firing" && state !== "pending")
return;
const count = state === "firing" || state === "pending" ? rule?.alerts?.length : 1;
if (stateName in statesPerGroup) {
statesPerGroup[stateName] += count;
} else {
statesPerGroup[stateName] = count;
}
});
if (filteredRules.length) {
const g = Object.assign({}, group);
g.rules = filteredRules;
g.states = statesPerGroup;
filteredGroups.push(g);
}
});
return { filteredGroups, allTypes, allStates };
};

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