mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 08:36:55 +03:00
Compare commits
9 Commits
query-debu
...
configwatc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0625bb77d | ||
|
|
4163f18250 | ||
|
|
686289c02b | ||
|
|
9ae10247bb | ||
|
|
06ce3f1496 | ||
|
|
d0690ba15f | ||
|
|
483e00ffb9 | ||
|
|
06f969a4a7 | ||
|
|
9517f5cf1a |
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/configwatcher"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmagent/csvimport"
|
||||
@@ -112,6 +113,7 @@ func main() {
|
||||
flag.Usage = usage
|
||||
envflag.Parse()
|
||||
remotewrite.InitSecretFlags()
|
||||
configwatcher.Init()
|
||||
buildinfo.Init()
|
||||
logger.Init()
|
||||
timeserieslimits.Init(*maxLabelsPerTimeseries, *maxLabelNameLen, *maxLabelValueLen)
|
||||
@@ -199,6 +201,7 @@ func main() {
|
||||
}
|
||||
protoparserutil.StopUnmarshalWorkers()
|
||||
remotewrite.Stop()
|
||||
configwatcher.Stop()
|
||||
|
||||
logger.Infof("successfully stopped vmagent in %.3f seconds", time.Since(startTime).Seconds())
|
||||
}
|
||||
|
||||
@@ -29,6 +29,18 @@ 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()
|
||||
|
||||
@@ -22,10 +22,11 @@ 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
|
||||
addr *url.URL
|
||||
argFunc AlertURLGenerator
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
lastError string
|
||||
|
||||
authCfg *promauth.Config
|
||||
// stores already parsed RelabelConfigs object
|
||||
@@ -71,6 +72,10 @@ 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))
|
||||
@@ -79,6 +84,9 @@ 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
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ 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 "" }
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ 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()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ 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"
|
||||
|
||||
@@ -30,6 +30,8 @@ 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"},
|
||||
@@ -195,6 +197,20 @@ 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
|
||||
@@ -209,6 +225,18 @@ 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 {
|
||||
@@ -337,12 +365,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)
|
||||
}
|
||||
@@ -459,8 +487,9 @@ func (rh *requestHandler) listNotifiers() ([]byte, error) {
|
||||
}
|
||||
for _, target := range protoTargets {
|
||||
notifier.Targets = append(notifier.Targets, &apiTarget{
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
Address: target.Addr(),
|
||||
Labels: target.Labels.ToMap(),
|
||||
LastError: target.LastError(),
|
||||
})
|
||||
}
|
||||
lr.Data.Notifiers = append(lr.Data.Notifiers, notifier)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,6 +25,7 @@ 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",
|
||||
@@ -45,7 +46,9 @@ 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{})
|
||||
m.groups[g.CreateID()] = g
|
||||
id := g.CreateID()
|
||||
m.groups[id] = g
|
||||
groupIDs = append(groupIDs, id)
|
||||
}
|
||||
rh := &requestHandler{m: m}
|
||||
|
||||
@@ -188,6 +191,21 @@ 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) {
|
||||
|
||||
@@ -28,6 +28,8 @@ 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
|
||||
@@ -109,11 +111,16 @@ 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
|
||||
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)
|
||||
}
|
||||
|
||||
// groupAlerts represents a group of alerts for WEB view
|
||||
|
||||
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
1
app/vmselect/vmui/assets/index-B7vIex3g.css
Normal file
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
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
205
app/vmselect/vmui/assets/index-SqjehVXD.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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-Ck5nH8JI.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-BVRvRxZ2.js">
|
||||
<script type="module" crossorigin src="./assets/index-SqjehVXD.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BHg4iVVe.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B7vIex3g.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine3.19
|
||||
FROM node:22-alpine3.22
|
||||
|
||||
# Sets a custom location for the npm cache, preventing access errors in system directories
|
||||
ENV NPM_CONFIG_CACHE=/build/.npm
|
||||
|
||||
489
app/vmui/packages/vmui/package-lock.json
generated
489
app/vmui/packages/vmui/package-lock.json
generated
@@ -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==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -1188,24 +1188,36 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"devOptional": 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==",
|
||||
"dev": true,
|
||||
"devOptional": 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==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1250,6 +1262,316 @@
|
||||
"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",
|
||||
@@ -1750,9 +2072,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
|
||||
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2221,7 +2543,7 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -2523,7 +2845,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -2572,6 +2894,14 @@
|
||||
"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",
|
||||
@@ -2702,6 +3032,23 @@
|
||||
"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",
|
||||
@@ -2750,6 +3097,14 @@
|
||||
"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",
|
||||
@@ -3060,6 +3415,20 @@
|
||||
"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",
|
||||
@@ -3808,7 +4177,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -4522,7 +4891,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -4577,7 +4946,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -4616,7 +4985,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -5119,7 +5488,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -5184,6 +5553,14 @@
|
||||
"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",
|
||||
@@ -5512,7 +5889,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5754,6 +6131,21 @@
|
||||
"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",
|
||||
@@ -6054,6 +6446,28 @@
|
||||
"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",
|
||||
@@ -6583,6 +6997,29 @@
|
||||
"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",
|
||||
@@ -6849,6 +7286,26 @@
|
||||
"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",
|
||||
@@ -6959,7 +7416,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==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
|
||||
@@ -18,84 +18,96 @@ 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>
|
||||
<Route
|
||||
path={"/"}
|
||||
element={<MainLayout/>}
|
||||
>
|
||||
return (
|
||||
<>
|
||||
<HashRouter>
|
||||
<AppContextProvider>
|
||||
<>
|
||||
<ThemeProvider onLoaded={setLoadedTheme} />
|
||||
{loadedTheme && (
|
||||
<Routes>
|
||||
<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>
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
</AppContextProvider>
|
||||
</HashRouter>
|
||||
</>;
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const getAccountIds = (server: string) =>
|
||||
`${server.replace(/^(.+)(\/select.+)/, "$1")}/admin/tenants`;
|
||||
import { getUrlWithoutTenant } from "../utils/tenants";
|
||||
export const getAccountIds = (server: string) => `${getUrlWithoutTenant(server)}/admin/tenants`;
|
||||
|
||||
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
23
app/vmui/packages/vmui/src/api/explore-alerts.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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`;
|
||||
};
|
||||
@@ -30,7 +30,13 @@ const delayOptions: AutoRefreshOption[] = [
|
||||
{ seconds: 7200, title: "2h" }
|
||||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
interface ExecutionControlsProps {
|
||||
tooltip: string;
|
||||
useAutorefresh?: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const ExecutionControls: FC<ExecutionControlsProps> = ({ tooltip, useAutorefresh, closeModal }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const dispatch = useTimeDispatch();
|
||||
@@ -56,6 +62,9 @@ export const ExecutionControls: FC = () => {
|
||||
|
||||
const handleUpdate = () => {
|
||||
dispatch({ type: "RUN_QUERY" });
|
||||
if (!useAutorefresh && isMobile) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,91 +86,118 @@ export const ExecutionControls: FC = () => {
|
||||
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,
|
||||
})}
|
||||
>
|
||||
{!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}>
|
||||
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>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
ariaLabel={tooltip}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</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 => (
|
||||
{useAutorefresh && (
|
||||
<Popper
|
||||
open={openOptions}
|
||||
placement="bottom-right"
|
||||
onClose={handleCloseOptions}
|
||||
buttonRef={optionsButtonRef}
|
||||
title={isMobile ? "Auto-refresh duration" : undefined}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-list-item": true,
|
||||
"vm-list-item_mobile": isMobile,
|
||||
"vm-list-item_active": d.seconds === selectedDelay.seconds
|
||||
"vm-execution-controls-list": true,
|
||||
"vm-execution-controls-list_mobile": isMobile,
|
||||
})}
|
||||
key={d.seconds}
|
||||
onClick={createHandlerChange(d)}
|
||||
>
|
||||
{d.title}
|
||||
{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>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</>;
|
||||
</Popper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-radius: calc($button-radius + 1px);
|
||||
min-width: 107px;
|
||||
|
||||
:is(.vm-autorefresh) {
|
||||
min-width: 107px;
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
@@ -0,0 +1,69 @@
|
||||
@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;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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";
|
||||
|
||||
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>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</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;
|
||||
@@ -0,0 +1,74 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
a:hover > pre {
|
||||
background-color: $color-background-badge;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: $color-background-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
@@ -0,0 +1,78 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
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";
|
||||
|
||||
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>
|
||||
<pre>
|
||||
<code className="language-promql">{query}</code>
|
||||
</pre>
|
||||
</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;
|
||||
@@ -0,0 +1,88 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
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-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
@@ -0,0 +1,60 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
@@ -0,0 +1,70 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
@@ -0,0 +1,40 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
@@ -0,0 +1,65 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
@@ -0,0 +1,18 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
@@ -0,0 +1,65 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
@@ -0,0 +1,48 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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(" "));
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
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
|
||||
@@ -14,21 +16,24 @@ const Accordion: FC<AccordionProps> = ({
|
||||
defaultExpanded = false,
|
||||
onChange,
|
||||
title,
|
||||
children
|
||||
children,
|
||||
id,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultExpanded);
|
||||
|
||||
const toggleOpen = () => {
|
||||
const toggleOpen = (event: JSX.TargetedMouseEvent<HTMLElement>) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString()) {
|
||||
if ((event.target as HTMLElement).closest("button")) {
|
||||
event.preventDefault();
|
||||
return; // If the text is selected, cancel the execution of toggle.
|
||||
}
|
||||
|
||||
setIsOpen((prev) => {
|
||||
const newState = !prev;
|
||||
onChange && onChange(newState);
|
||||
return newState;
|
||||
});
|
||||
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);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,23 +42,23 @@ const Accordion: FC<AccordionProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`vm-accordion-header ${isOpen && "vm-accordion-header_open"}`}
|
||||
onClick={toggleOpen}
|
||||
<details
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
open={isOpen}
|
||||
id={id}
|
||||
>
|
||||
{title}
|
||||
<div className={`vm-accordion-header__arrow ${isOpen && "vm-accordion-header__arrow_open"}`}>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</header>
|
||||
{isOpen && (
|
||||
<section
|
||||
className="vm-accordion-section"
|
||||
key="content"
|
||||
<summary
|
||||
className="vm-accordion-header"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
)}
|
||||
{title}
|
||||
<div className="vm-accordion-header__arrow">
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</summary>
|
||||
{children}
|
||||
</details>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
transform: rotate(0);
|
||||
transition: transform 200ms ease-in-out;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: auto;
|
||||
@@ -28,6 +24,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vm-accordion-section[open] > summary {
|
||||
& > .vm-accordion-header {
|
||||
&__arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,27 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-alert {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
position: sticky;
|
||||
top: $padding-global;
|
||||
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;
|
||||
@@ -48,54 +34,53 @@
|
||||
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;
|
||||
|
||||
&:after {
|
||||
background-color: $color-success;
|
||||
}
|
||||
background-color: $color-background-success;
|
||||
}
|
||||
|
||||
&_error {
|
||||
color: $color-error;
|
||||
|
||||
&:after {
|
||||
background-color: $color-error;
|
||||
}
|
||||
background-color: $color-background-error;
|
||||
}
|
||||
|
||||
&_info {
|
||||
color: $color-info;
|
||||
|
||||
&:after {
|
||||
background-color: $color-info;
|
||||
}
|
||||
background-color: $color-background-info;
|
||||
}
|
||||
|
||||
&_warning {
|
||||
color: $color-warning;
|
||||
|
||||
&:after {
|
||||
background-color: $color-warning;
|
||||
}
|
||||
background-color: $color-background-warning;
|
||||
}
|
||||
|
||||
&_dark {
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&_dark &__content {
|
||||
&_dark &__content, &_dark &__icon {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AutocompleteOptions {
|
||||
}
|
||||
|
||||
interface AutocompleteProps {
|
||||
itemClassName?: string
|
||||
value: string
|
||||
options: AutocompleteOptions[]
|
||||
anchor: React.RefObject<HTMLElement>
|
||||
@@ -41,6 +42,7 @@ enum FocusType {
|
||||
|
||||
const Autocomplete: FC<AutocompleteProps> = ({
|
||||
value,
|
||||
itemClassName,
|
||||
options,
|
||||
anchor,
|
||||
disabled,
|
||||
@@ -212,7 +214,9 @@ const Autocomplete: FC<AutocompleteProps> = ({
|
||||
>
|
||||
{selected?.includes(option.value) && <DoneIcon/>}
|
||||
<>{option.icon}</>
|
||||
<span>{option.value}</span>
|
||||
<div className={`vm-list-item-inner ${itemClassName} ${option.value.toLowerCase().replace(" ", "-")}`}>
|
||||
<span>{option.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
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"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -9,7 +10,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
|
||||
interface ModalProps {
|
||||
title?: string
|
||||
title: JSX.Element | string
|
||||
children: ReactNode
|
||||
onClose: () => void
|
||||
className?: string
|
||||
|
||||
@@ -5,10 +5,11 @@ import { MouseEvent } from "react";
|
||||
|
||||
interface MultipleSelectedValueProps {
|
||||
values: string[]
|
||||
itemClassName?: string
|
||||
onRemoveItem: (val: string) => void
|
||||
}
|
||||
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemoveItem }) => {
|
||||
const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, itemClassName, onRemoveItem }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const createHandleClick = (value: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
@@ -27,7 +28,7 @@ const MultipleSelectedValue: FC<MultipleSelectedValueProps> = ({ values, onRemov
|
||||
return <>
|
||||
{values.map(item => (
|
||||
<div
|
||||
className="vm-select-input-content__selected"
|
||||
className={`vm-select-input-content__selected ${itemClassName} ${item.toLowerCase().replace(" ", "-")}`}
|
||||
key={item}
|
||||
>
|
||||
<span>{item}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@ import useEventListener from "../../../hooks/useEventListener";
|
||||
import useClickOutside from "../../../hooks/useClickOutside";
|
||||
|
||||
interface SelectProps {
|
||||
itemClassName?: string
|
||||
value: string | string[]
|
||||
list: string[]
|
||||
label?: string
|
||||
@@ -20,6 +21,7 @@ interface SelectProps {
|
||||
searchable?: boolean
|
||||
autofocus?: boolean
|
||||
disabled?: boolean
|
||||
includeAll?: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -27,12 +29,14 @@ const Select: FC<SelectProps> = ({
|
||||
value,
|
||||
list,
|
||||
label,
|
||||
itemClassName,
|
||||
placeholder,
|
||||
noOptionsText,
|
||||
clearable = false,
|
||||
searchable = false,
|
||||
autofocus,
|
||||
disabled,
|
||||
includeAll,
|
||||
onChange
|
||||
}) => {
|
||||
const { isDarkTheme } = useAppState();
|
||||
@@ -46,7 +50,7 @@ const Select: FC<SelectProps> = ({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMultiple = Array.isArray(value);
|
||||
const selectedValues = Array.isArray(value) ? value : undefined;
|
||||
const selectedValues = Array.isArray(value) ? value.slice() : [];
|
||||
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
|
||||
|
||||
const textFieldValue = useMemo(() => {
|
||||
@@ -119,6 +123,9 @@ 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({
|
||||
@@ -135,11 +142,12 @@ const Select: FC<SelectProps> = ({
|
||||
<div className="vm-select-input-content">
|
||||
{!!selectedValues?.length && (
|
||||
<MultipleSelectedValue
|
||||
itemClassName={itemClassName}
|
||||
values={selectedValues}
|
||||
onRemoveItem={handleSelected}
|
||||
/>
|
||||
)}
|
||||
{!hideInput && (
|
||||
{!hideInput && !selectedValues?.length && (
|
||||
<input
|
||||
value={textFieldValue}
|
||||
type="text"
|
||||
@@ -171,9 +179,10 @@ const Select: FC<SelectProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<Autocomplete
|
||||
itemClassName={itemClassName}
|
||||
label={label}
|
||||
value={autocompleteValue}
|
||||
options={list.map(el => ({ value: el }))}
|
||||
options={list.map(l => ({ value: l }))}
|
||||
anchor={autocompleteAnchorEl}
|
||||
selected={selectedValues}
|
||||
minLength={1}
|
||||
|
||||
17
app/vmui/packages/vmui/src/constants/alerts.ts
Normal file
17
app/vmui/packages/vmui/src/constants/alerts.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RuleType } from "../types";
|
||||
|
||||
export const RULE_TYPES: RuleType[] = [
|
||||
{
|
||||
id: "alerts",
|
||||
title: "Alerts",
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
title: "Recording",
|
||||
id: "recording",
|
||||
},
|
||||
{
|
||||
title: "All",
|
||||
id: "all",
|
||||
},
|
||||
];
|
||||
@@ -7,4 +7,11 @@ 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;
|
||||
|
||||
|
||||
export const isDefaultDatasourceType = (datasourceType: string): boolean => {
|
||||
switch (APP_TYPE) {
|
||||
case AppType.victoriametrics:
|
||||
return datasourceType == "prometheus" || datasourceType == "";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
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",
|
||||
@@ -25,15 +33,23 @@ export const darkPalette = {
|
||||
};
|
||||
|
||||
export const lightPalette = {
|
||||
"color-primary": "#3F51B5",
|
||||
"color-secondary": "#E91E63",
|
||||
"color-error": "#FD080E",
|
||||
"color-warning": "#FF8308",
|
||||
"color-info": "#03A9F4",
|
||||
"color-success": "#4CAF50",
|
||||
"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-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",
|
||||
|
||||
@@ -18,7 +18,7 @@ const useFetchFlags = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const url = getUrlWithoutTenant(serverUrl).replace(/\/prometheus\/?$/, "");
|
||||
const url = getUrlWithoutTenant(serverUrl);
|
||||
const response = await fetch(`${url}/flags`);
|
||||
const data = await response.text();
|
||||
const flags = data.split("\n").filter(flag => flag.trim() !== "")
|
||||
|
||||
@@ -7,12 +7,20 @@ const useSearchParamsFromObject = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const setSearchParamsFromKeys = useCallback((objectParams: Record<string, string | number>) => {
|
||||
const hasSearchParams = !!Array.from(searchParams.values()).length;
|
||||
const hasSearchParams = !!searchParams.size;
|
||||
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 (searchParams.get(key) !== `${value}`) {
|
||||
searchParams.set(key, `${value}`);
|
||||
if (newSearchParams.get(key) !== `${value}`) {
|
||||
newSearchParams.set(key, `${value}`);
|
||||
hasChanged = true;
|
||||
}
|
||||
});
|
||||
@@ -20,7 +28,7 @@ const useSearchParamsFromObject = () => {
|
||||
if (!hasChanged) return;
|
||||
|
||||
if (hasSearchParams) {
|
||||
setSearchParams(searchParams);
|
||||
setSearchParams(newSearchParams);
|
||||
} else {
|
||||
navigate(`?${searchParams.toString()}`, { replace: true });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,11 @@ const ControlsAnomalyLayout: FC<ControlsProps> = ({
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@ const Header: FC<HeaderProps> = ({ controlsComponent }) => {
|
||||
controlsComponent={controlsComponent}
|
||||
displaySidebar={displaySidebar}
|
||||
isMobile={isMobile}
|
||||
closeModal={() => {}}
|
||||
/>
|
||||
</header>;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ControlsProps {
|
||||
isMobile?: boolean;
|
||||
headerSetup?: RouterOptionsHeader;
|
||||
accountIds?: string[];
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
@@ -45,6 +46,7 @@ const HeaderControls: FC<ControlsProps & HeaderProps> = ({
|
||||
isMobile={isMobile}
|
||||
accountIds={accountIds}
|
||||
headerSetup={headerSetup}
|
||||
closeModal={handleCloseList}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ const ControlsMainLayout: FC<ControlsProps> = ({
|
||||
displaySidebar,
|
||||
isMobile,
|
||||
headerSetup,
|
||||
accountIds
|
||||
accountIds,
|
||||
closeModal,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
@@ -28,7 +29,11 @@ const ControlsMainLayout: FC<ControlsProps> = ({
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls
|
||||
tooltip={headerSetup?.executionControls?.tooltip}
|
||||
useAutorefresh={headerSetup?.executionControls?.useAutorefresh}
|
||||
closeModal={closeModal}
|
||||
/>}
|
||||
<GlobalSettings/>
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
@@ -0,0 +1,142 @@
|
||||
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;
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
206
app/vmui/packages/vmui/src/pages/ExploreAlerts/ExploreRules.tsx
Normal file
206
app/vmui/packages/vmui/src/pages/ExploreAlerts/ExploreRules.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
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;
|
||||
88
app/vmui/packages/vmui/src/pages/ExploreAlerts/helpers.ts
Normal file
88
app/vmui/packages/vmui/src/pages/ExploreAlerts/helpers.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getGroupUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
|
||||
interface FetchGroupReturn<T> {
|
||||
group?: T;
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
interface FetchGroupProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const useFetchGroup = <T>({
|
||||
id,
|
||||
}: FetchGroupProps): FetchGroupReturn<T> => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [group, setGroup] = useState<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(
|
||||
() => getGroupUrl(serverUrl, id),
|
||||
[serverUrl, id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
switch (response.headers.get("Content-Type")) {
|
||||
case "application/json": {
|
||||
const resp = await response.json();
|
||||
if (response.ok) {
|
||||
setGroup(resp as T);
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let err = await response.text();
|
||||
if (err.startsWith("unsupported path requested")) {
|
||||
err = `Failed to show group details. Request to ${fetchUrl} failed with error: ${err.trim()}.\nMake sure that vmalert is reachable at ${fetchUrl} and is of the same or higher version than vmselect`;
|
||||
} else {
|
||||
err = `${response.statusText}\r\n${err}`;
|
||||
}
|
||||
setError(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period]);
|
||||
|
||||
return { group, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getGroupsUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes, Group } from "../../../types";
|
||||
|
||||
interface FetchGroupsReturn {
|
||||
groups: Group[];
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
interface FetchGroupsProps {
|
||||
blockFetch: boolean
|
||||
}
|
||||
|
||||
export const useFetchGroups = ({ blockFetch }: FetchGroupsProps): FetchGroupsReturn => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(
|
||||
() => getGroupsUrl(serverUrl),
|
||||
[serverUrl],
|
||||
);
|
||||
|
||||
const loaded = !!groups.length || !blockFetch;
|
||||
|
||||
useEffect(() => {
|
||||
if (blockFetch) return;
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const data = (resp.data.groups || []) as Group[];
|
||||
setGroups(data.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period, loaded]);
|
||||
|
||||
return { groups, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getItemUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { ErrorTypes } from "../../../types";
|
||||
|
||||
interface FetchItemReturn<T> {
|
||||
item?: T;
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
interface FetchItemProps {
|
||||
groupId: string;
|
||||
id: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export const useFetchItem = <T>({
|
||||
groupId,
|
||||
id,
|
||||
mode,
|
||||
}: FetchItemProps): FetchItemReturn<T> => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [item, setItem] = useState<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(
|
||||
() => getItemUrl(serverUrl, groupId, id, mode),
|
||||
[serverUrl, groupId, id, mode],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setItem(resp as T);
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period]);
|
||||
|
||||
return { item, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import { useEffect, useMemo, useState } from "preact/compat";
|
||||
import { getNotifiersUrl } from "../../../api/explore-alerts";
|
||||
import { useAppState } from "../../../state/common/StateContext";
|
||||
import { Notifier, ErrorTypes } from "../../../types";
|
||||
|
||||
interface FetchNotifiersReturn {
|
||||
notifiers: Notifier[];
|
||||
isLoading: boolean;
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
export const useFetchNotifiers = (): FetchNotifiersReturn => {
|
||||
const { serverUrl } = useAppState();
|
||||
const { period } = useTimeState();
|
||||
|
||||
const [notifiers, setNotifiers] = useState<Notifier[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorTypes | string>();
|
||||
|
||||
const fetchUrl = useMemo(() => getNotifiersUrl(serverUrl), [serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
const resp = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const data = (resp.data.notifiers || []) as Notifier[];
|
||||
setNotifiers(data.sort((a, b) => a.kind.localeCompare(b.kind)));
|
||||
setError(undefined);
|
||||
} else {
|
||||
setError(`${resp.errorType}\r\n${resp?.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchData().catch(console.error);
|
||||
}, [fetchUrl, period]);
|
||||
|
||||
return { notifiers, isLoading, error };
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect } from "react";
|
||||
import { compactObject } from "../../../utils/object";
|
||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
||||
|
||||
interface rulesQueryProps {
|
||||
types?: string;
|
||||
states?: string;
|
||||
search?: string;
|
||||
rule_id: string;
|
||||
group_id: string;
|
||||
alert_id: string;
|
||||
}
|
||||
|
||||
export const useRulesSetQueryParams = ({
|
||||
types,
|
||||
states,
|
||||
search,
|
||||
rule_id,
|
||||
alert_id,
|
||||
group_id,
|
||||
}: rulesQueryProps) => {
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
|
||||
const setSearchParamsFromState = () => {
|
||||
const params = compactObject({
|
||||
types,
|
||||
states,
|
||||
search,
|
||||
alert_id,
|
||||
rule_id,
|
||||
group_id,
|
||||
});
|
||||
|
||||
setSearchParamsFromKeys(params);
|
||||
};
|
||||
|
||||
useEffect(setSearchParamsFromState, [
|
||||
types,
|
||||
states,
|
||||
search,
|
||||
rule_id,
|
||||
group_id,
|
||||
alert_id,
|
||||
]);
|
||||
};
|
||||
|
||||
interface notifiersQueryProps {
|
||||
kinds: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export const useNotifiersSetQueryParams = ({
|
||||
kinds,
|
||||
search,
|
||||
}: notifiersQueryProps) => {
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
|
||||
const setSearchParamsFromState = () => {
|
||||
const params = compactObject({
|
||||
kinds,
|
||||
search,
|
||||
});
|
||||
|
||||
setSearchParamsFromKeys(params);
|
||||
};
|
||||
|
||||
useEffect(setSearchParamsFromState, [kinds, search]);
|
||||
};
|
||||
77
app/vmui/packages/vmui/src/pages/ExploreAlerts/style.scss
Normal file
77
app/vmui/packages/vmui/src/pages/ExploreAlerts/style.scss
Normal file
@@ -0,0 +1,77 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-explore-alert-group {
|
||||
width: 100%;
|
||||
&:has(.vm-accordion-header_open) {
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts.vm-modal {
|
||||
align-items: center;
|
||||
.vm-explore-rule-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: $padding-tiny;
|
||||
}
|
||||
.vm-modal-content {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-list-item-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-tiny;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-items {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
padding: 10px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.vm-explore-alerts-notifier {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
row-gap: 10px;
|
||||
padding: $padding-tiny 0;
|
||||
}
|
||||
|
||||
.vm-explore-alerts {
|
||||
font-size: 12px;
|
||||
.vm-modal-content-header__title {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.vm-accordion-header {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
max-width: calc(100vw - var(--scrollbar-width));
|
||||
|
||||
@media (max-width: 500px) {
|
||||
gap: $padding-small;
|
||||
}
|
||||
|
||||
&-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
gap: $padding-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ const RawQueryPage: FC = () => {
|
||||
})}
|
||||
>
|
||||
<QueryConfigurator
|
||||
label={"Time series selector"}
|
||||
label="Time series selector"
|
||||
queryErrors={!hideError ? queryErrors : []}
|
||||
setQueryErrors={setQueryErrors}
|
||||
setHideError={setHideError}
|
||||
|
||||
@@ -15,20 +15,28 @@ const router = {
|
||||
rawQuery: "/raw-query",
|
||||
downsamplingDebug: "/downsampling-filters-debug",
|
||||
retentionDebug: "/retention-filters-debug",
|
||||
alerts: "/alerts",
|
||||
rules: "/rules",
|
||||
notifiers: "/notifiers",
|
||||
};
|
||||
|
||||
export interface RouterOptionsHeader {
|
||||
tenant?: boolean,
|
||||
stepControl?: boolean,
|
||||
timeSelector?: boolean,
|
||||
executionControls?: boolean,
|
||||
globalSettings?: boolean,
|
||||
cardinalityDatePicker?: boolean
|
||||
tenant?: boolean;
|
||||
stepControl?: boolean;
|
||||
timeSelector?: boolean;
|
||||
executionControls?: ExecutionControlsProps;
|
||||
globalSettings?: boolean;
|
||||
cardinalityDatePicker?: boolean;
|
||||
}
|
||||
|
||||
export interface RouterOptions {
|
||||
title?: string,
|
||||
header: RouterOptionsHeader
|
||||
title?: string;
|
||||
header: RouterOptionsHeader;
|
||||
}
|
||||
|
||||
interface ExecutionControlsProps {
|
||||
tooltip: string;
|
||||
useAutorefresh: boolean;
|
||||
}
|
||||
|
||||
const routerOptionsDefault = {
|
||||
@@ -36,18 +44,21 @@ const routerOptionsDefault = {
|
||||
tenant: true,
|
||||
stepControl: true,
|
||||
timeSelector: true,
|
||||
executionControls: true,
|
||||
}
|
||||
executionControls: {
|
||||
tooltip: "Refresh dashboard",
|
||||
useAutorefresh: true,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
[router.home]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.rawQuery]: {
|
||||
title: "Raw query",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.metrics]: {
|
||||
title: "Explore Prometheus metrics",
|
||||
@@ -55,65 +66,83 @@ export const routerOptions: { [key: string]: RouterOptions } = {
|
||||
tenant: true,
|
||||
stepControl: true,
|
||||
timeSelector: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.cardinality]: {
|
||||
title: "Explore cardinality",
|
||||
header: {
|
||||
tenant: true,
|
||||
cardinalityDatePicker: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.topQueries]: {
|
||||
title: "Top queries",
|
||||
header: {
|
||||
tenant: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.trace]: {
|
||||
title: "Trace analyzer",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.queryAnalyzer]: {
|
||||
title: "Query analyzer",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.dashboards]: {
|
||||
title: "Dashboards",
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.rules]: {
|
||||
title: "Rules",
|
||||
header: {
|
||||
executionControls: {
|
||||
tooltip: "Refresh alerts",
|
||||
useAutorefresh: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
[router.notifiers]: {
|
||||
title: "Notifiers",
|
||||
header: {
|
||||
executionControls: {
|
||||
tooltip: "Refresh notifiers",
|
||||
useAutorefresh: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
[router.withTemplate]: {
|
||||
title: "WITH templates",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.relabel]: {
|
||||
title: "Metric relabel debug",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.activeQueries]: {
|
||||
title: "Active Queries",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.icons]: {
|
||||
title: "Icons",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.anomaly]: {
|
||||
title: "Anomaly exploration",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.query]: {
|
||||
title: "Query",
|
||||
...routerOptionsDefault
|
||||
...routerOptionsDefault,
|
||||
},
|
||||
[router.downsamplingDebug]: {
|
||||
title: "Downsampling filters debug",
|
||||
header: {}
|
||||
header: {},
|
||||
},
|
||||
[router.retentionDebug]: {
|
||||
title: "Retention filters debug",
|
||||
header: {}
|
||||
}
|
||||
header: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import router, { routerOptions } from "./index";
|
||||
import { getTenantIdFromUrl } from "../utils/tenants";
|
||||
|
||||
export enum NavigationItemType {
|
||||
internalLink,
|
||||
@@ -21,21 +20,6 @@ interface NavigationConfig {
|
||||
showAlertLink: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case for alert link
|
||||
*/
|
||||
const getAlertLink = (url: string, showAlertLink: boolean) => {
|
||||
// see more https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert
|
||||
const isCluster = !!getTenantIdFromUrl(url);
|
||||
const value = isCluster ? `${url}/vmalert` : url.replace(/\/prometheus$/, "/vmalert");
|
||||
return {
|
||||
label: "Alerts",
|
||||
value,
|
||||
type: NavigationItemType.externalLink,
|
||||
hide: !showAlertLink,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Submenu for Tools tab
|
||||
*/
|
||||
@@ -58,11 +42,19 @@ const getExploreNav = () => [
|
||||
{ value: router.activeQueries },
|
||||
];
|
||||
|
||||
/**
|
||||
* Submenu for Alerts tab
|
||||
*/
|
||||
|
||||
const getAlertingNav = () => [
|
||||
{ value: router.rules },
|
||||
{ value: router.notifiers },
|
||||
];
|
||||
|
||||
/**
|
||||
* Default navigation menu
|
||||
*/
|
||||
export const getDefaultNavigation = ({
|
||||
serverUrl,
|
||||
isEnterpriseLicense,
|
||||
showPredefinedDashboards,
|
||||
showAlertLink,
|
||||
@@ -72,7 +64,7 @@ export const getDefaultNavigation = ({
|
||||
{ label: "Explore", submenu: getExploreNav() },
|
||||
{ label: "Tools", submenu: getToolsNav(isEnterpriseLicense) },
|
||||
{ value: router.dashboards, hide: !showPredefinedDashboards },
|
||||
getAlertLink(serverUrl, showAlertLink),
|
||||
{ value: "Alerting", submenu: getAlertingNav(), hide: !showAlertLink },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
animation: vm-scale 150ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||
}
|
||||
|
||||
span {
|
||||
.vm-list-item-inner {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
--color-primary: #3F51B5;
|
||||
--color-secondary: #E91E63;
|
||||
--color-error: #FD080E;
|
||||
--color-broken: #FF8C33;
|
||||
--color-warning: #FF8308;
|
||||
--color-info: #03A9F4;
|
||||
--color-success: #4CAF50;
|
||||
--color-notice: #BD33FF;
|
||||
--color-keyword: rgba(161, 15, 94, 1);
|
||||
|
||||
/* text palette */
|
||||
--color-primary-text: #FFFFFF;
|
||||
@@ -45,4 +48,5 @@
|
||||
|
||||
--border-divider: 1px solid rgba(0, 0, 0, 0.15);
|
||||
--color-hover-black: rgba(0, 0, 0, 0.06);
|
||||
--color-hover-darker: rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
$color-primary: var(--color-primary);
|
||||
$color-secondary: var(--color-secondary);
|
||||
$color-error: var(--color-error);
|
||||
$color-background-error: var(--color-background-error);
|
||||
$color-warning: var(--color-warning);
|
||||
$color-background-warning: var(--color-background-warning);
|
||||
$color-info: var(--color-info);
|
||||
$color-background-info: var(--color-background-info);
|
||||
$color-success: var(--color-success);
|
||||
$color-background-success: var(--color-background-success);
|
||||
$color-notice: var(--color-notice);
|
||||
$color-passive: var(--color-passive);
|
||||
$color-broken: var(--color-broken);
|
||||
$color-keyword: var(--color-keyword);
|
||||
|
||||
$color-primary-text: var(--color-primary-text);
|
||||
$color-secondary-text: var(--color-secondary-text);
|
||||
@@ -28,15 +36,21 @@ $color-tropical-blue: #C9E3F6;
|
||||
$color-background-body: var(--color-background-body);
|
||||
$color-background-block: var(--color-background-block);
|
||||
$color-background-tooltip: var(--color-background-tooltip);
|
||||
|
||||
$color-background-item: var(--color-background-item);
|
||||
$color-background-badge: var(--color-background-badge);
|
||||
$color-background-hover: var(--color-background-hover);
|
||||
|
||||
/************* padding *************/
|
||||
$padding-global: 12px;
|
||||
$padding-large: 16px;
|
||||
$padding-medium: 12px;
|
||||
$padding-small: 8px;
|
||||
$padding-tiny: 6px;
|
||||
|
||||
|
||||
/************* line height *************/
|
||||
$line-height-table: 40px;
|
||||
|
||||
/************* fonts *************/
|
||||
$font-family-global: system-ui;
|
||||
$font-family-monospace: monospace;
|
||||
@@ -61,4 +75,5 @@ $box-shadow: var(--box-shadow);
|
||||
$box-shadow-popper: var(--box-shadow-popper);
|
||||
|
||||
$color-hover-black: var(--color-hover-black);
|
||||
$color-hover-darker: var(--color-hover-darker);
|
||||
$vh: var(--vh);
|
||||
|
||||
@@ -30,17 +30,17 @@ export interface DataValue {
|
||||
value: number; // y axis value
|
||||
}
|
||||
|
||||
export interface DataSeries extends MetricBase{
|
||||
export interface DataSeries extends MetricBase {
|
||||
metadata: {
|
||||
name: string;
|
||||
},
|
||||
};
|
||||
values: DataValue[]; // sorted by key which is timestamp
|
||||
}
|
||||
|
||||
export interface InstantDataSeries {
|
||||
metadata: string[]; // just ordered columns
|
||||
value: string;
|
||||
values: string[]
|
||||
values: string[];
|
||||
copyValue: string;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface PanelSettings {
|
||||
expr: string[];
|
||||
alias?: string[];
|
||||
showLegend?: boolean;
|
||||
width?: number
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface DashboardRow {
|
||||
@@ -77,55 +77,61 @@ export interface DashboardSettings {
|
||||
}
|
||||
|
||||
export interface RelativeTimeOption {
|
||||
id: string,
|
||||
duration: string,
|
||||
until: () => Date,
|
||||
title: string,
|
||||
isDefault?: boolean,
|
||||
id: string;
|
||||
duration: string;
|
||||
until: () => Date;
|
||||
title: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface TopQuery {
|
||||
accountID: number
|
||||
avgDurationSeconds: number
|
||||
count: number
|
||||
projectID: number
|
||||
query: string
|
||||
timeRangeSeconds: number
|
||||
sumDurationSeconds: number
|
||||
timeRange: string
|
||||
url?: string
|
||||
accountID: number;
|
||||
avgDurationSeconds: number;
|
||||
count: number;
|
||||
projectID: number;
|
||||
query: string;
|
||||
timeRangeSeconds: number;
|
||||
sumDurationSeconds: number;
|
||||
timeRange: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface TopQueryStats {
|
||||
"search.queryStats.lastQueriesCount": number
|
||||
"search.queryStats.minQueryDuration": string
|
||||
"search.queryStats.lastQueriesCount": number;
|
||||
"search.queryStats.minQueryDuration": string;
|
||||
}
|
||||
|
||||
export interface TopQueriesData extends TopQueryStats {
|
||||
maxLifetime: string
|
||||
topN: string
|
||||
topByAvgDuration: TopQuery[]
|
||||
topByCount: TopQuery[]
|
||||
topBySumDuration: TopQuery[]
|
||||
error?: string
|
||||
maxLifetime: string;
|
||||
topN: string;
|
||||
topByAvgDuration: TopQuery[];
|
||||
topByCount: TopQuery[];
|
||||
topBySumDuration: TopQuery[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SeriesLimits {
|
||||
table: number,
|
||||
chart: number,
|
||||
code: number,
|
||||
table: number;
|
||||
chart: number;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface Timezone {
|
||||
region: string,
|
||||
utc: string,
|
||||
search?: string
|
||||
region: string;
|
||||
utc: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface GraphSize {
|
||||
id: string,
|
||||
isDefault?: boolean,
|
||||
height: () => number
|
||||
id: string;
|
||||
isDefault?: boolean;
|
||||
height: () => number;
|
||||
}
|
||||
|
||||
export interface RuleType {
|
||||
id: string;
|
||||
title: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export enum Theme {
|
||||
@@ -141,7 +147,7 @@ export interface RelabelStep {
|
||||
errors: {
|
||||
inLabels: string;
|
||||
outLabels: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface RelabelData {
|
||||
@@ -173,5 +179,84 @@ export enum QueryContextType {
|
||||
export interface AppConfig {
|
||||
license?: {
|
||||
type?: "enterprise" | "opensource";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
name: string;
|
||||
file: string;
|
||||
rules: Rule[];
|
||||
interval: number;
|
||||
limit: number;
|
||||
lastEvaluation: number;
|
||||
evaluationTime: number;
|
||||
type: string;
|
||||
id: string;
|
||||
concurrency: number;
|
||||
params: string[];
|
||||
headers: string[];
|
||||
notifier_headers: string[];
|
||||
labels: Record<string, string>;
|
||||
eval_offset: number;
|
||||
eval_delay: number;
|
||||
states: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
state: string;
|
||||
name: string;
|
||||
query: string;
|
||||
duration: number;
|
||||
keepFiringFor: number;
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
alerts: Alert[];
|
||||
health: string;
|
||||
lastEvaluation: number;
|
||||
lastError: string;
|
||||
evaluationTime: number;
|
||||
type: string;
|
||||
datasourceType: string;
|
||||
lastSamples: bigint;
|
||||
lastSeriesFetched: bigint;
|
||||
id: string;
|
||||
group_id: string;
|
||||
debug: boolean;
|
||||
updates: RuleUpdate[];
|
||||
max_updates_entries: number;
|
||||
}
|
||||
|
||||
interface RuleUpdate {
|
||||
time: string;
|
||||
at: string;
|
||||
duration: number;
|
||||
error: string;
|
||||
samples: number;
|
||||
series_fetched: number;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
name: string;
|
||||
state: string;
|
||||
value: string;
|
||||
group_id: string;
|
||||
expression: string;
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
activeAt: number;
|
||||
id: string;
|
||||
source: string;
|
||||
restored: boolean;
|
||||
stabilizing: boolean;
|
||||
}
|
||||
|
||||
export interface Notifier {
|
||||
kind: string;
|
||||
targets: Target[];
|
||||
}
|
||||
|
||||
export interface Target {
|
||||
address: string;
|
||||
labels: Record<string, string>;
|
||||
lastError: string;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export const getDefaultServer = (tenantId?: string): string => {
|
||||
const { serverURL } = getAppModeParams();
|
||||
const storageURL = getFromStorage("SERVER_URL") as string;
|
||||
const anomalyURL = `${window.location.origin}${window.location.pathname.replace(/^\/vmui/, "")}`;
|
||||
const defaultURL = window.location.href.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "/prometheus");
|
||||
const baseURL = window.location.href.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "");
|
||||
const defaultURL = baseURL.replace(/(\/select\/[\d:]+)$/, "$1/prometheus");
|
||||
const url = serverURL || storageURL || defaultURL;
|
||||
|
||||
switch (APP_TYPE) {
|
||||
|
||||
@@ -10,33 +10,24 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
|
||||
switch (playground) {
|
||||
case "METRICS": {
|
||||
return {
|
||||
"^/vmalert/.*": {
|
||||
"^/(api|vmalert)/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
"/flags": {
|
||||
target: "https://play.victoriametrics.com",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"^/api/.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/prometheus/",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
"^/prometheus/api.*": {
|
||||
target: "https://play.victoriametrics.com/select/0/",
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on("error", (err) => {
|
||||
console.error("[proxy error]", err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
default: {
|
||||
@@ -48,10 +39,7 @@ const getProxy = (): Record<string, ProxyOptions> | undefined => {
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
base: "",
|
||||
plugins: [
|
||||
preact(),
|
||||
dynamicIndexHtmlPlugin({ mode })
|
||||
],
|
||||
plugins: [preact(), dynamicIndexHtmlPlugin({ mode })],
|
||||
assetsInclude: ["**/*.md"],
|
||||
server: {
|
||||
open: true,
|
||||
@@ -60,7 +48,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"src": path.resolve(__dirname, "src"),
|
||||
src: path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
@@ -71,12 +59,9 @@ export default defineConfig(({ mode }) => {
|
||||
if (id.includes("node_modules")) {
|
||||
return "vendor";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#
|
||||
# See https://github.com/timescale/tsbs/blob/master/docs/victoriametrics.md
|
||||
# for details
|
||||
tsbs: tsbs-build tsbs-generate-data tsbs-load-data tsbs-generate-queries tsbs-run-queries
|
||||
tsbs: tsbs-build tsbs-generate-data tsbs-load-data tsbs-generate-queries-all tsbs-run-queries-all
|
||||
|
||||
TSBS_SCALE ?= 100000
|
||||
TSBS_END ?= $(shell date -u +%Y-%m-%dT00:00:00Z)
|
||||
@@ -45,8 +45,9 @@ TSBS_START ?= $(shell \
|
||||
TSBS_STEP ?= 80s
|
||||
TSBS_QUERIES ?= 1000
|
||||
TSBS_WORKERS ?= 4
|
||||
TSBS_QUERY_TYPE := cpu-max-all-8
|
||||
TSBS_DATA_FILE := /tmp/tsbs-data-$(TSBS_SCALE)-$(TSBS_START)-$(TSBS_END)-$(TSBS_STEP).gz
|
||||
TSBS_QUERY_FILE := /tmp/tsbs-queries-$(TSBS_SCALE)-$(TSBS_START)-$(TSBS_END)-$(TSBS_QUERIES).gz
|
||||
TSBS_QUERY_FILE := /tmp/tsbs-queries-$(TSBS_QUERY_TYPE)-$(TSBS_SCALE)-$(TSBS_START)-$(TSBS_END)-$(TSBS_QUERIES).gz
|
||||
# For cluster setup use http://vminsert:8480/insert/0/influx/write
|
||||
TSBS_WRITE_URLS ?= http://localhost:8428/write
|
||||
# For cluster setup use http://vmselect:8481/select/0/prometheus
|
||||
@@ -86,7 +87,7 @@ tsbs-load-data:
|
||||
-e process_io_written_bytes_total || true
|
||||
|
||||
|
||||
# Generate benchmark queries
|
||||
# Generate benchmark queries of a given type
|
||||
tsbs-generate-queries:
|
||||
test -f $(TSBS_QUERY_FILE) || /tmp/tsbs/bin/tsbs_generate_queries \
|
||||
--format=victoriametrics \
|
||||
@@ -95,10 +96,40 @@ tsbs-generate-queries:
|
||||
--scale=$(TSBS_SCALE) \
|
||||
--timestamp-start=$(TSBS_START) \
|
||||
--timestamp-end=$(TSBS_END) \
|
||||
--query-type=cpu-max-all-8 \
|
||||
--query-type=$(TSBS_QUERY_TYPE) \
|
||||
--queries=1000 \
|
||||
| gzip > $(TSBS_QUERY_FILE)
|
||||
|
||||
# Run benchmark queries against VictoriaMetrics
|
||||
# Generate all types of benchmark queries
|
||||
tsbs-generate-queries-all:
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=single-groupby-1-1-1
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=single-groupby-1-1-12
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=single-groupby-1-8-1
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=single-groupby-5-1-1
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=single-groupby-5-1-12
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=single-groupby-5-8-1
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=cpu-max-all-1
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=cpu-max-all-8
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=double-groupby-1
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=double-groupby-5
|
||||
$(MAKE) tsbs-generate-queries TSBS_QUERY_TYPE=double-groupby-all
|
||||
|
||||
# Run benchmark queries of a given type against VictoriaMetrics
|
||||
tsbs-run-queries:
|
||||
cat $(TSBS_QUERY_FILE) | gunzip | /tmp/tsbs/bin/tsbs_run_queries_victoriametrics --workers=$(TSBS_WORKERS) --urls=$(TSBS_READ_URLS)
|
||||
|
||||
# Run all types of benchmark queries against VictoriaMetrics
|
||||
tsbs-run-queries-all:
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=single-groupby-1-1-1
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=single-groupby-1-1-12
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=single-groupby-1-8-1
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=single-groupby-5-1-1
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=single-groupby-5-1-12
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=single-groupby-5-8-1
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=cpu-max-all-1
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=cpu-max-all-8
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=double-groupby-1
|
||||
$(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=double-groupby-5
|
||||
|
||||
# Too heavy: retrieves 1B samples
|
||||
# $(MAKE) tsbs-run-queries TSBS_QUERY_TYPE=double-groupby-all
|
||||
|
||||
@@ -1133,8 +1133,6 @@ Below is the output for `/path/to/vminsert -help`:
|
||||
Whether to disable re-routing when some of vmstorage nodes are unavailable. Disabled re-routing stops ingestion when some storage nodes are unavailable. On the other side, disabled re-routing minimizes the number of active time series in the cluster during rolling restarts and during spikes in series churn rate. See also -disableRerouting
|
||||
-dropSamplesOnOverload
|
||||
Whether to drop incoming samples if the destination vmstorage node is overloaded and/or unavailable. This prioritizes cluster availability over consistency, e.g. the cluster continues accepting all the ingested samples, but some of them may be dropped if vmstorage nodes are temporarily unavailable and/or overloaded. The drop of samples happens before the replication, so it's not recommended to use this flag with -replicationFactor enabled.
|
||||
-enableMetadata
|
||||
Whether to enable metadata processing for metrics scraped from targets, received via VictoriaMetrics remote write, Prometheus remote write v1 or OpenTelemetry protocol. See also remoteWrite.maxMetadataPerBlock
|
||||
-enableTCP6
|
||||
Whether to enable IPv6 for listening and dialing. By default, only IPv4 TCP and UDP are used
|
||||
-envflag.enable
|
||||
@@ -1313,113 +1311,6 @@ Below is the output for `/path/to/vminsert -help`:
|
||||
Flag value can be read from the given file when using -pprofAuthKey=file:///abs/path/to/file or -pprofAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -pprofAuthKey=http://host/path or -pprofAuthKey=https://host/path
|
||||
-prevCacheRemovalPercent float
|
||||
Items in the previous caches are removed when the percent of requests it serves becomes lower than this value. Higher values reduce memory usage at the cost of higher CPU usage. See also -cacheExpireDuration (default 0.1)
|
||||
-promscrape.azureSDCheckInterval duration
|
||||
Interval for checking for changes in Azure. This works only if azure_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#azure_sd_configs for details (default 1m0s)
|
||||
-promscrape.cluster.memberLabel string
|
||||
If non-empty, then the label with this name and the -promscrape.cluster.memberNum value is added to all the scraped metrics. See https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets for more info
|
||||
-promscrape.cluster.memberNum string
|
||||
The number of vmagent instance in the cluster of scrapers. It must be a unique value in the range 0 ... promscrape.cluster.membersCount-1 across scrapers in the cluster. Can be specified as pod name of Kubernetes StatefulSet - pod-name-Num, where Num is a numeric part of pod name. See also -promscrape.cluster.memberLabel . See https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets for more info (default "0")
|
||||
-promscrape.cluster.memberURLTemplate string
|
||||
An optional template for URL to access vmagent instance with the given -promscrape.cluster.memberNum value. Every %d occurrence in the template is substituted with -promscrape.cluster.memberNum at urls to vmagent instances responsible for scraping the given target at /service-discovery page. For example -promscrape.cluster.memberURLTemplate='http://vmagent-%d:8429/targets'. See https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets for more details
|
||||
-promscrape.cluster.membersCount int
|
||||
The number of members in a cluster of scrapers. Each member must have a unique -promscrape.cluster.memberNum in the range 0 ... promscrape.cluster.membersCount-1 . Each member then scrapes roughly 1/N of all the targets. By default, cluster scraping is disabled, i.e. a single scraper scrapes all the targets. See https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets for more info (default 1)
|
||||
-promscrape.cluster.name string
|
||||
Optional name of the cluster. If multiple vmagent clusters scrape the same targets, then each cluster must have unique name in order to properly de-duplicate samples received from these clusters. See https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets for more info
|
||||
-promscrape.cluster.replicationFactor int
|
||||
The number of members in the cluster, which scrape the same targets. If the replication factor is greater than 1, then the deduplication must be enabled at remote storage side. See https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets for more info (default 1)
|
||||
-promscrape.config string
|
||||
Optional path to Prometheus config file with 'scrape_configs' section containing targets to scrape. The path can point to local file and to http url. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#how-to-scrape-prometheus-exporters-such-as-node-exporter for details
|
||||
-promscrape.config.dryRun
|
||||
Checks -promscrape.config file for errors and unsupported fields and then exits. Returns non-zero exit code on parsing errors and emits these errors to stderr. See also -promscrape.config.strictParse command-line flag. Pass -loggerLevel=ERROR if you don't need to see info messages in the output.
|
||||
-promscrape.config.strictParse
|
||||
Whether to deny unsupported fields in -promscrape.config . Set to false in order to silently skip unsupported fields (default true)
|
||||
-promscrape.configCheckInterval duration
|
||||
Interval for checking for changes in -promscrape.config file. By default, the checking is disabled. See how to reload -promscrape.config file at https://docs.victoriametrics.com/victoriametrics/vmagent/#configuration-update
|
||||
-promscrape.consul.waitTime duration
|
||||
Wait time used by Consul service discovery. Default value is used if not set
|
||||
-promscrape.consulSDCheckInterval duration
|
||||
Interval for checking for changes in Consul. This works only if consul_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#consul_sd_configs for details (default 30s)
|
||||
-promscrape.consulagentSDCheckInterval duration
|
||||
Interval for checking for changes in Consul Agent. This works only if consulagent_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#consulagent_sd_configs for details (default 30s)
|
||||
-promscrape.digitaloceanSDCheckInterval duration
|
||||
Interval for checking for changes in digital ocean. This works only if digitalocean_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#digitalocean_sd_configs for details (default 1m0s)
|
||||
-promscrape.disableCompression
|
||||
Whether to disable sending 'Accept-Encoding: gzip' request headers to all the scrape targets. This may reduce CPU usage on scrape targets at the cost of higher network bandwidth utilization. It is possible to set 'disable_compression: true' individually per each 'scrape_config' section in '-promscrape.config' for fine-grained control
|
||||
-promscrape.disableKeepAlive
|
||||
Whether to disable HTTP keep-alive connections when scraping all the targets. This may be useful when targets has no support for HTTP keep-alive connection. It is possible to set 'disable_keepalive: true' individually per each 'scrape_config' section in '-promscrape.config' for fine-grained control. Note that disabling HTTP keep-alive may increase load on both vmagent and scrape targets
|
||||
-promscrape.discovery.concurrency int
|
||||
The maximum number of concurrent requests to Prometheus autodiscovery API (Consul, Kubernetes, etc.) (default 100)
|
||||
-promscrape.discovery.concurrentWaitTime duration
|
||||
The maximum duration for waiting to perform API requests if more than -promscrape.discovery.concurrency requests are simultaneously performed (default 1m0s)
|
||||
-promscrape.dnsSDCheckInterval duration
|
||||
Interval for checking for changes in dns. This works only if dns_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#dns_sd_configs for details (default 30s)
|
||||
-promscrape.dockerSDCheckInterval duration
|
||||
Interval for checking for changes in docker. This works only if docker_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#docker_sd_configs for details (default 30s)
|
||||
-promscrape.dockerswarmSDCheckInterval duration
|
||||
Interval for checking for changes in dockerswarm. This works only if dockerswarm_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#dockerswarm_sd_configs for details (default 30s)
|
||||
-promscrape.dropOriginalLabels
|
||||
Whether to drop original labels for scrape targets at /targets and /api/v1/targets pages. This may be needed for reducing memory usage when original labels for big number of scrape targets occupy big amounts of memory. Note that this reduces debuggability for improper per-target relabeling configs
|
||||
-promscrape.ec2SDCheckInterval duration
|
||||
Interval for checking for changes in ec2. This works only if ec2_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#ec2_sd_configs for details (default 1m0s)
|
||||
-promscrape.eurekaSDCheckInterval duration
|
||||
Interval for checking for changes in eureka. This works only if eureka_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#eureka_sd_configs for details (default 30s)
|
||||
-promscrape.fileSDCheckInterval duration
|
||||
Interval for checking for changes in 'file_sd_config'. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#file_sd_configs for details (default 1m0s)
|
||||
-promscrape.gceSDCheckInterval duration
|
||||
Interval for checking for changes in gce. This works only if gce_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#gce_sd_configs for details (default 1m0s)
|
||||
-promscrape.hetznerSDCheckInterval duration
|
||||
Interval for checking for changes in Hetzner API. This works only if hetzner_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#hetzner_sd_configs for details (default 1m0s)
|
||||
-promscrape.httpSDCheckInterval duration
|
||||
Interval for checking for changes in http endpoint service discovery. This works only if http_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#http_sd_configs for details (default 1m0s)
|
||||
-promscrape.kubernetes.apiServerTimeout duration
|
||||
How frequently to reload the full state from Kubernetes API server (default 30m0s)
|
||||
-promscrape.kubernetes.attachNodeMetadataAll
|
||||
Whether to set attach_metadata.node=true for all the kubernetes_sd_configs at -promscrape.config . It is possible to set attach_metadata.node=false individually per each kubernetes_sd_configs . See https://docs.victoriametrics.com/victoriametrics/sd_configs/#kubernetes_sd_configs
|
||||
-promscrape.kubernetes.useHTTP2Client
|
||||
Whether to use HTTP/2 client for connection to Kubernetes API server. This may reduce amount of concurrent connections to API server when watching for a big number of Kubernetes objects.
|
||||
-promscrape.kubernetesSDCheckInterval duration
|
||||
Interval for checking for changes in Kubernetes API server. This works only if kubernetes_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#kubernetes_sd_configs for details (default 30s)
|
||||
-promscrape.kumaSDCheckInterval duration
|
||||
Interval for checking for changes in kuma service discovery. This works only if kuma_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#kuma_sd_configs for details (default 30s)
|
||||
-promscrape.marathonSDCheckInterval duration
|
||||
Interval for checking for changes in Marathon REST API. This works only if marathon_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#marathon_sd_configs for details (default 30s)
|
||||
-promscrape.maxDroppedTargets int
|
||||
The maximum number of droppedTargets to show at /api/v1/targets page. Increase this value if your setup drops more scrape targets during relabeling and you need investigating labels for all the dropped targets. Note that the increased number of tracked dropped targets may result in increased memory usage (default 10000)
|
||||
-promscrape.maxResponseHeadersSize size
|
||||
The maximum size of http response headers from Prometheus scrape targets
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 4096)
|
||||
-promscrape.maxScrapeSize size
|
||||
The maximum size of scrape response in bytes to process from Prometheus targets. Bigger responses are rejected. See also max_scrape_size option at https://docs.victoriametrics.com/victoriametrics/sd_configs/#scrape_configs
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 16777216)
|
||||
-promscrape.minResponseSizeForStreamParse size
|
||||
The minimum target response size for automatic switching to stream parsing mode, which can reduce memory usage. See https://docs.victoriametrics.com/victoriametrics/vmagent/#stream-parsing-mode
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 1000000)
|
||||
-promscrape.noStaleMarkers
|
||||
Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. This option also disables populating the scrape_series_added metric. See https://prometheus.io/docs/concepts/jobs_instances/#automatically-generated-labels-and-time-series
|
||||
-promscrape.nomad.waitTime duration
|
||||
Wait time used by Nomad service discovery. Default value is used if not set
|
||||
-promscrape.nomadSDCheckInterval duration
|
||||
Interval for checking for changes in Nomad. This works only if nomad_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#nomad_sd_configs for details (default 30s)
|
||||
-promscrape.openstackSDCheckInterval duration
|
||||
Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#openstack_sd_configs for details (default 30s)
|
||||
-promscrape.ovhcloudSDCheckInterval duration
|
||||
Interval for checking for changes in OVH Cloud API. This works only if ovhcloud_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#ovhcloud_sd_configs for details (default 30s)
|
||||
-promscrape.puppetdbSDCheckInterval duration
|
||||
Interval for checking for changes in PuppetDB API. This works only if puppetdb_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#puppetdb_sd_configs for details (default 30s)
|
||||
-promscrape.seriesLimitPerTarget int
|
||||
Optional limit on the number of unique time series a single scrape target can expose. See https://docs.victoriametrics.com/victoriametrics/vmagent/#cardinality-limiter for more info
|
||||
-promscrape.streamParse
|
||||
Whether to enable stream parsing for metrics obtained from scrape targets. This may be useful for reducing memory usage when millions of metrics are exposed per each scrape target. It is possible to set 'stream_parse: true' individually per each 'scrape_config' section in '-promscrape.config' for fine-grained control
|
||||
-promscrape.suppressDuplicateScrapeTargetErrors
|
||||
Whether to suppress 'duplicate scrape target' errors; see https://docs.victoriametrics.com/victoriametrics/vmagent/#troubleshooting for details
|
||||
-promscrape.suppressScrapeErrors
|
||||
Whether to suppress scrape errors logging. The last error for each target is always available at '/targets' page even if scrape errors logging is suppressed. See also -promscrape.suppressScrapeErrorsDelay
|
||||
-promscrape.suppressScrapeErrorsDelay duration
|
||||
The delay for suppressing repeated scrape errors logging per each scrape targets. This may be used for reducing the number of log lines related to scrape errors. See also -promscrape.suppressScrapeErrors
|
||||
-promscrape.vultrSDCheckInterval duration
|
||||
Interval for checking for changes in Vultr. This works only if vultr_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#vultr_sd_configs for details (default 30s)
|
||||
-promscrape.yandexcloudSDCheckInterval duration
|
||||
Interval for checking for changes in Yandex Cloud API. This works only if yandexcloud_sd_configs is configured in '-promscrape.config' file. See https://docs.victoriametrics.com/victoriametrics/sd_configs/#yandexcloud_sd_configs for details (default 30s)
|
||||
-pushmetrics.disableCompression
|
||||
Whether to disable request body compression when pushing metrics to every -pushmetrics.url
|
||||
-pushmetrics.extraLabel array
|
||||
@@ -1437,7 +1328,7 @@ Below is the output for `/path/to/vminsert -help`:
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
Value can contain comma inside single-quoted or double-quoted string, {}, [] and () braces.
|
||||
-relabelConfig string
|
||||
Optional path to a file with relabeling rules, which are applied to all the ingested metrics. The path can point either to local file or to http url. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#relabeling for details. The config is reloaded on SIGHUP signal orwith periodicity that is defined via -relabel.configCheckInterval
|
||||
Optional path to a file with relabeling rules, which are applied to all the ingested metrics. The path can point either to local file or to http url. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#relabeling for details. The config is reloaded on SIGHUP signal
|
||||
-relabelConfigCheckInterval duration
|
||||
Interval for checking for changes in '-relabelConfig' file. By default the checking is disabled. Send SIGHUP signal in order to force config check for changes
|
||||
-replicationFactor int
|
||||
@@ -1445,7 +1336,7 @@ Below is the output for `/path/to/vminsert -help`:
|
||||
-rpc.disableCompression
|
||||
Whether to disable compression for the data sent from vminsert to vmstorage. This reduces CPU usage at the cost of higher network bandwidth usage
|
||||
-rpc.handshakeTimeout duration
|
||||
Timeout for RPC handshake between vminsert/vmselect and vmstorage. Increase this value if transient handshake failures occur. See https://docs.victoriametrics.com/victoriametrics/troubleshooting/#cluster-instability section for more details. (default 5s)
|
||||
Timeout for RPC handshake between vminsert/vmselect and vmstorage. Increase this value if transient handshake failures occur. (default 5s)
|
||||
-search.denyPartialResponse
|
||||
Whether to deny partial responses if a part of -storageNode instances fail to perform queries; this trades availability over consistency; see also -search.maxQueryDuration
|
||||
-sortLabels
|
||||
@@ -1520,7 +1411,7 @@ Below is the output for `/path/to/vmselect -help`:
|
||||
-clusternative.disableCompression
|
||||
Whether to disable compression of the data sent to vmselect via -clusternativeListenAddr. This reduces CPU usage at the cost of higher network bandwidth usage
|
||||
-clusternative.maxConcurrentRequests int
|
||||
The maximum number of concurrent vmselect requests the server can process at -clusternativeListenAddr. It shouldn't be high, since a single request usually saturates a CPU core at the underlying vmstorage nodes, and many concurrently executed requests may require high amounts of memory. See also -clusternative.maxQueueDuration
|
||||
The maximum number of concurrent vmselect requests the server can process at -clusternativeListenAddr. Default value depends on the number of available CPU cores. It shouldn't be high, since a single request usually saturates a CPU core at the underlying vmstorage nodes, and many concurrently executed requests may require high amounts of memory. See also -clusternative.maxQueueDuration
|
||||
-clusternative.maxQueueDuration duration
|
||||
The maximum time the incoming query to -clusternativeListenAddr waits for execution when -clusternative.maxConcurrentRequests limit is reached (default 10s)
|
||||
-clusternative.maxTagKeys int
|
||||
@@ -1688,7 +1579,7 @@ Below is the output for `/path/to/vmselect -help`:
|
||||
How many copies of every ingested sample is available across -storageNode nodes. vmselect continues returning full responses when up to replicationFactor-1 vmstorage nodes are temporarily unavailable. See also -globalReplicationFactor and -search.skipSlowReplicas (default 1)
|
||||
Supports an array of `key:value` entries separated by comma or specified via multiple flags.
|
||||
-rpc.handshakeTimeout duration
|
||||
Timeout for RPC handshake between vminsert/vmselect and vmstorage. Increase this value if transient handshake failures occur. See https://docs.victoriametrics.com/victoriametrics/troubleshooting/#cluster-instability section for more details. (default 5s)
|
||||
Timeout for RPC handshake between vminsert/vmselect and vmstorage. Increase this value if transient handshake failures occur. (default 5s)
|
||||
-search.cacheTimestampOffset duration
|
||||
The maximum duration since the current time for response data, which is always queried from the original raw data, without using the response cache. Increase this value if you see gaps in responses due to time synchronization issues between VictoriaMetrics and data sources (default 5m0s)
|
||||
-search.denyPartialResponse
|
||||
@@ -1718,7 +1609,7 @@ Below is the output for `/path/to/vmselect -help`:
|
||||
-search.logSlowQueryStats duration
|
||||
Log query statistics if execution time exceeding this value - see https://docs.victoriametrics.com/victoriametrics/query-stats . Zero disables slow query statistics logging. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
-search.logSlowQueryStatsHeaders array
|
||||
White list of header keys to log for queries exceeding -search.logSlowQueryStats. Case insensitive. By default, no headers are logged. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
White list of header keys to log for queries exceeding -search.logSlowQueryStats. By default, no headers are logged. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
Value can contain comma inside single-quoted or double-quoted string, {}, [] and () braces.
|
||||
-search.maxBinaryOpPushdownLabelValues instance
|
||||
@@ -1983,7 +1874,8 @@ Below is the output for `/path/to/vmstorage -help`:
|
||||
Whether to log new series. This option is for debug purposes only. It can lead to performance issues when big number of new series are ingested into VictoriaMetrics
|
||||
-logNewSeriesAuthKey value
|
||||
authKey, which must be passed in query string to /internal/log_new_series. It overrides -httpAuth.*
|
||||
Flag value can be read from the given file when using -logNewSeriesAuthKey=file:///abs/path/to/file or -logNewSeriesAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -logNewSeriesAuthKey=http://host/path or -logNewSeriesAuthKey=https://host/path
|
||||
Flag value can be read from the given file when using -logNewSeriesAuthKey=file:///abs/path/to/file or -logNewSeriesAuthKey=file://./relative/path/to/file .
|
||||
Flag value can be read from the given http/https url when using -logNewSeriesAuthKey=http://host/path or -logNewSeriesAuthKey=https://host/path
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
@@ -2057,7 +1949,7 @@ Below is the output for `/path/to/vmstorage -help`:
|
||||
-rpc.disableCompression
|
||||
Whether to disable compression of the data sent from vmstorage to vmselect. This reduces CPU usage at the cost of higher network bandwidth usage
|
||||
-rpc.handshakeTimeout duration
|
||||
Timeout for RPC handshake between vminsert/vmselect and vmstorage. Increase this value if transient handshake failures occur. See https://docs.victoriametrics.com/victoriametrics/troubleshooting/#cluster-instability section for more details. (default 5s)
|
||||
Timeout for RPC handshake between vminsert/vmselect and vmstorage. Increase this value if transient handshake failures occur. (default 5s)
|
||||
-search.maxConcurrentRequests int
|
||||
The maximum number of concurrent vmselect requests the vmstorage can process at -vmselectAddr. It shouldn't be high, since a single request usually saturates a CPU core, and many concurrently executed requests may require high amounts of memory. See also -search.maxQueueDuration
|
||||
-search.maxQueueDuration duration
|
||||
@@ -2095,16 +1987,11 @@ Below is the output for `/path/to/vmstorage -help`:
|
||||
-storage.cacheSizeMetricNamesStats size
|
||||
Overrides max size for storage/metricNamesStatsTracker cache. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeStorageMetricName size
|
||||
Overrides max size for storage/metricName cache. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeStorageTSID size
|
||||
Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.finalDedupScheduleCheckInterval duration
|
||||
The interval for checking when final deduplication process should be started.Storage unconditionally adds 25% jitter to the interval value on each check evaluation. Changing the interval to the bigger values may delay downsampling, deduplication for historical data. See also https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication (default 1h0m0s)
|
||||
-storage.idbPrefillStart duration
|
||||
Specifies how early VictoriaMetrics starts pre-filling indexDB records before indexDB rotation. Starting the pre-fill process earlier can help reduce resource usage spikes during rotation. In most cases, this value should not be changed. The maximum allowed value is 23h. (default 1h0m0s)
|
||||
-storage.maxDailySeries int
|
||||
The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cardinality-limiter . See also -storage.maxHourlySeries
|
||||
-storage.maxHourlySeries int
|
||||
|
||||
@@ -292,6 +292,9 @@ VMUI provides the following features:
|
||||
- [Metric relabel debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/relabeling) - debug [relabeling](#relabeling) rules.
|
||||
- [Downsampling filters debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/downsampling-filters-debug) {{% available_from "v1.105.0" %}} - debug [downsampling](#downsampling) configs.
|
||||
- [Retention filters debugger](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus/graph/#/retention-filters-debug) {{% available_from "v1.105.0" %}} - debug [retention filter](#retention-filters) configs.
|
||||
- `Alerting` {{% available_from "#" %}} for displaying groups and rules from the [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/) service.
|
||||
The tab is available only if [VictoriaMetrics single-node](https://docs.victoriametrics.com/#vmalert) or
|
||||
[vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert) are configured with `-vmalert.proxyURL` command-line flag.
|
||||
|
||||
**Querying**
|
||||
|
||||
@@ -2450,8 +2453,6 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
Value can contain comma inside single-quoted or double-quoted string, {}, [] and () braces.
|
||||
-dryRun
|
||||
Whether to check config files without running VictoriaMetrics. The following config files are checked: -promscrape.config, -relabelConfig and -streamAggr.config. Unknown config entries aren't allowed in -promscrape.config by default. This can be changed with -promscrape.config.strictParse=false command-line flag
|
||||
-enableMetadata
|
||||
Whether to enable metadata processing for metrics scraped from targets, received via VictoriaMetrics remote write, Prometheus remote write v1 or OpenTelemetry protocol. See also remoteWrite.maxMetadataPerBlock
|
||||
-enableTCP6
|
||||
Whether to enable IPv6 for listening and dialing. By default, only IPv4 TCP and UDP are used
|
||||
-envflag.enable
|
||||
@@ -2569,7 +2570,8 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
Whether to log new series. This option is for debug purposes only. It can lead to performance issues when big number of new series are ingested into VictoriaMetrics
|
||||
-logNewSeriesAuthKey value
|
||||
authKey, which must be passed in query string to /internal/log_new_series. It overrides -httpAuth.*
|
||||
Flag value can be read from the given file when using -logNewSeriesAuthKey=file:///abs/path/to/file or -logNewSeriesAuthKey=file://./relative/path/to/file . Flag value can be read from the given http/https url when using -logNewSeriesAuthKey=http://host/path or -logNewSeriesAuthKey=https://host/path
|
||||
Flag value can be read from the given file when using -logNewSeriesAuthKey=file:///abs/path/to/file or -logNewSeriesAuthKey=file://./relative/path/to/file .
|
||||
Flag value can be read from the given http/https url when using -logNewSeriesAuthKey=http://host/path or -logNewSeriesAuthKey=https://host/path
|
||||
-loggerDisableTimestamps
|
||||
Whether to disable writing timestamps in logs
|
||||
-loggerErrorsPerSecondLimit int
|
||||
@@ -2818,7 +2820,7 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-search.logSlowQueryStats duration
|
||||
Log query statistics if execution time exceeding this value - see https://docs.victoriametrics.com/victoriametrics/query-stats . Zero disables slow query statistics logging. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
-search.logSlowQueryStatsHeaders array
|
||||
White list of header keys to log for queries exceeding -search.logSlowQueryStats. Case insensitive. By default, no headers are logged. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
White list of header keys to log for queries exceeding -search.logSlowQueryStats. By default, no headers are logged. This flag is available only in VictoriaMetrics enterprise. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
Value can contain comma inside single-quoted or double-quoted string, {}, [] and () braces.
|
||||
-search.maxBinaryOpPushdownLabelValues instance
|
||||
@@ -2945,16 +2947,14 @@ Pass `-help` to VictoriaMetrics in order to see the list of supported command-li
|
||||
-storage.cacheSizeMetricNamesStats size
|
||||
Overrides max size for storage/metricNamesStatsTracker cache. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeStorageMetricName size
|
||||
Overrides max size for storage/metricName cache. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.cacheSizeStorageTSID size
|
||||
Overrides max size for storage/tsid cache. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cache-tuning
|
||||
Supports the following optional suffixes for size values: KB, MB, GB, TB, KiB, MiB, GiB, TiB (default 0)
|
||||
-storage.finalDedupScheduleCheckInterval duration
|
||||
The interval for checking when final deduplication process should be started.Storage unconditionally adds 25% jitter to the interval value on each check evaluation. Changing the interval to the bigger values may delay downsampling, deduplication for historical data. See also https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#deduplication (default 1h0m0s)
|
||||
-storage.idbPrefillStart duration
|
||||
Specifies how early VictoriaMetrics starts pre-filling indexDB records before indexDB rotation. Starting the pre-fill process earlier can help reduce resource usage spikes during rotation. In most cases, this value should not be changed. The maximum allowed value is 23h. (default 1h0m0s)
|
||||
Specifies how early VictoriaMetrics starts pre-filling indexDB records before indexDB rotation. Starting the pre-fill process earlier can help reduce resource usage spikes during rotation.
|
||||
In most cases, this value should not be changed. The maximum allowed value is 23h. (default 1h0m0s)
|
||||
-storage.maxDailySeries int
|
||||
The maximum number of unique series can be added to the storage during the last 24 hours. Excess series are logged and dropped. This can be useful for limiting series churn rate. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#cardinality-limiter . See also -storage.maxHourlySeries
|
||||
-storage.maxHourlySeries int
|
||||
|
||||
@@ -26,6 +26,9 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
* FEATURE: upgrade Go builder from Go1.24.6 to Go1.25. See [Go1.25 release notes](https://tip.golang.org/doc/go1.25).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add export functionality for Query (Table view) and RawQuery tabs in CSV/JSON format. See [#9332](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9332).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): replace `Alerts` tab with `Alerting` tab in vmui. The new `Alerting` tab displays vmalert groups and rules directly in vmui interface without redirecting user to vmalert's WEB UI. Links of format `.*/prometheus/vmalert/.*` will continue working by redirecting to vmalert's UI. This functionality is available only if `-vmalert.proxyURL` is set on vmselect. Some functionality of the new `Alerting` tab requires vmalert to be of the same version as vmselect, or higher.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `/api/v1/group?group_id=<id>` API endpoint for viewing details of a specific [rules group](https://docs.victoriametrics.com/vmalert.html#groups). The new handler is used by new `Alerting` tab in vmselect.
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `lastError` field to `/api/v1/notifiers` response. The new field contains an error message if the last attempt to send data to the notifier failed. The error can be also viewed in `Alerting. Notifiers` tab.
|
||||
|
||||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): prevent remote write ingestion stop on push error for [Google Pub/Sub](https://docs.victoriametrics.com/victoriametrics/vmagent/#writing-metrics-to-pubsub) integration.
|
||||
* BUGFIX: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): properly handle [mTLS authorization and routing](https://docs.victoriametrics.com/victoriametrics/vmauth/#mtls-based-request-routing). Previously it didn't work. See [#29](https://github.com/VictoriaMetrics/VictoriaLogs/issues/29).
|
||||
@@ -40,7 +43,7 @@ Released at 2025-08-15
|
||||
* SECURITY: upgrade Go builder from Go1.24.5 to Go1.24.6. See [the list of issues addressed in Go1.24.6](https://github.com/golang/go/issues?q=milestone%3AGo1.24.6+label%3ACherryPickApproved).
|
||||
|
||||
* FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and [vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): protect graphite `/render` API endpoint with new flag `-search.maxGraphitePathExpressionLen`. See this PR [#9534](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9534) for details.
|
||||
* FEATURE: expose `vm_total_disk_space_bytes` metric at the [`/metrics` page](https://docs.victoriametrics.com/#monitoring), which shows the total disk space for the data directory specified via [`-storageDataPath`](https://docs.victoriametrics.com/#storage). This metric can be useful for building alerts and graphs for the percentatge of free disk space via `vm_free_disk_space_bytes / vm_total_disk_space_bytes`. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9523#issuecomment-3149459926).
|
||||
* FEATURE: expose `vm_total_disk_space_bytes` metric at the [`/metrics` page](https://docs.victoriametrics.com/#monitoring), which shows the total disk space for the data directory specified via [`-storageDataPath`](https://docs.victoriametrics.com/#storage). This metric can be useful for building alerts and graphs for the percentage of free disk space via `vm_free_disk_space_bytes / vm_total_disk_space_bytes`. See [this comment](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9523#issuecomment-3149459926).
|
||||
* FEATURE: all: leave non-existing environment variables as is in config files instead of failure. For example, if the file referred by `-promscrape.config` contains `%{NON_EXISTING_ENV_VAR}` placeholder, then it is left as is instead of failing to load the file. This simplifies the usage of environment variables in config files and in command-line flags according to [these docs](https://docs.victoriametrics.com/victoriametrics/#environment-variables). Users can easily notice non-existing env vars in config files and in command-line flags by looking at their values - they will literally contain `%{NON_EXISTING_ENV_VAR}` strings.
|
||||
* FEATURE: `vmselect`, `vminsert` and `vmstorage` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): add rpc handshake timeout configuration via `-rpc.handshakeTimeout` flag (default 5s). Set deadline for the entire handshake process instead of per-operation timeout. See [#9345](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9345) for more details.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): add `-enableMetadata` command-line flag to allow sending metadata to the configured `-remoteWrite.url`, metadata can be scraped from targets, received via VictoriaMetrics remote write, Prometheus remote write v1 or OpenTelemetry protocol. See [#2974](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2974).
|
||||
|
||||
@@ -1484,7 +1484,7 @@ It is safe sharing the collected profiles from security point of view, since the
|
||||
```bash
|
||||
./vmagent -help
|
||||
|
||||
vmagent collects metrics data via popular data ingestion protocols and routes it to VictoriaMetrics.
|
||||
vmagent collects metrics data via popular data ingestion protocols and routes them to VictoriaMetrics.
|
||||
|
||||
See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
|
||||
|
||||
@@ -1556,7 +1556,7 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
Empty values are set to false.
|
||||
-gcp.pubsub.subscribe.topicSubscription.messageFormat array
|
||||
Message format for the corresponding -gcp.pubsub.subcribe.topicSubscription. Valid formats: influx, prometheus, promremotewrite, graphite, jsonline . See https://docs.victoriametrics.com/victoriametrics/vmagent/#reading-metrics-from-pubsub . This flag is available only in Enterprise binaries. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
Message format for the corresponding -gcp.pubsub.subscribe.topicSubscription. Valid formats: influx, prometheus, promremotewrite, graphite, jsonline . See https://docs.victoriametrics.com/victoriametrics/vmagent/#reading-metrics-from-pubsub . This flag is available only in Enterprise binaries. See https://docs.victoriametrics.com/victoriametrics/enterprise/
|
||||
Supports an array of values separated by comma or specified via multiple flags.
|
||||
Value can contain comma inside single-quoted or double-quoted string, {}, [] and () braces.
|
||||
-graphite.sanitizeMetricName
|
||||
@@ -2018,16 +2018,12 @@ See the docs at https://docs.victoriametrics.com/victoriametrics/vmagent/ .
|
||||
Empty values are set to default value.
|
||||
-remoteWrite.relabelConfig string
|
||||
Optional path to file with relabeling configs, which are applied to all the metrics before sending them to -remoteWrite.url. See also -remoteWrite.urlRelabelConfig. The path can point either to local file or to http url. See https://docs.victoriametrics.com/victoriametrics/relabeling/
|
||||
-remoteWrite.retryMaxInterval array
|
||||
The maximum delay between retry attempts to send a block of data to the corresponding -remoteWrite.url. The delay doubles with each retry until this maximum is reached, after which it remains constant. See also -remoteWrite.retryMinInterval (default 1m0s)
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
Empty values are set to default value.
|
||||
-remoteWrite.retryMaxTime array
|
||||
The max time spent on retry attempts to send a block of data to the corresponding -remoteWrite.url. This flag is deprecated, use -remoteWrite.retryMaxInterval instead (default 1m0s)
|
||||
The max time spent on retry attempts to send a block of data to the corresponding -remoteWrite.url. Change this value if it is expected for -remoteWrite.url to be unreachable for more than -remoteWrite.retryMaxTime. See also -remoteWrite.retryMinInterval (default 1m0s)
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
Empty values are set to default value.
|
||||
-remoteWrite.retryMinInterval array
|
||||
The minimum delay between retry attempts to send a block of data to the corresponding -remoteWrite.url. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxInterval (default 1s)
|
||||
The minimum delay between retry attempts to send a block of data to the corresponding -remoteWrite.url. Every next retry attempt will double the delay to prevent hammering of remote database. See also -remoteWrite.retryMaxTime (default 1s)
|
||||
Supports array of values separated by comma or specified via multiple flags.
|
||||
Empty values are set to default value.
|
||||
-remoteWrite.roundDigits array
|
||||
|
||||
@@ -26,7 +26,7 @@ Configure `-vmalert.proxyURL` on VictoriaMetrics [single-node](https://docs.vict
|
||||
or [vmselect in cluster version](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert)
|
||||
to proxy requests to `vmalert`. Proxying is needed for the following cases:
|
||||
* to proxy requests from [Grafana Alerting UI](https://grafana.com/docs/grafana/latest/alerting/);
|
||||
* to access `vmalert`'s UI through [VictoriaMetrics Web interface](https://docs.victoriametrics.com/#vmui).
|
||||
* to access `vmalert`'s UI through [vmui](https://docs.victoriametrics.com/#vmui).
|
||||
|
||||
[VictoriaMetrics Cloud](https://console.victoriametrics.cloud/signUp?utm_source=website&utm_campaign=docs_vm_vmalert_intro)
|
||||
provides out-of-the-box alerting functionality based on `vmalert`. This service simplifies the setup
|
||||
@@ -740,7 +740,9 @@ or time series modification via [relabeling](https://docs.victoriametrics.com/vi
|
||||
* `http://<vmalert-addr>/api/v1/alerts` - list of all active alerts;
|
||||
* `http://<vmalert-addr>/api/v1/notifiers` - list all available notifiers;
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/alert?group_id=<group_id>&alert_id=<alert_id>` - get alert status in JSON format.
|
||||
Used as alert source in AlertManager.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&rule_id=<rule_id>` - get rule status in JSON format.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/group?group_id=<group_id>` - get group status in JSON format.
|
||||
Used as alert source in AlertManager.
|
||||
* `http://<vmalert-addr>/vmalert/alert?group_id=<group_id>&alert_id=<alert_id>` - get alert status in web UI.
|
||||
* `http://<vmalert-addr>/vmalert/rule?group_id=<group_id>&rule_id=<rule_id>` - get rule status in web UI.
|
||||
* `http://<vmalert-addr>/vmalert/api/v1/rule?group_id=<group_id>&alert_id=<alert_id>` - get rule status in JSON format.
|
||||
|
||||
2
go.mod
2
go.mod
@@ -49,7 +49,7 @@ require (
|
||||
github.com/valyala/fastjson v1.6.4
|
||||
github.com/valyala/fastrand v1.1.0
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
github.com/valyala/gozstd v1.22.0
|
||||
github.com/valyala/gozstd v1.23.2
|
||||
github.com/valyala/histogram v1.2.0
|
||||
github.com/valyala/quicktemplate v1.8.0
|
||||
golang.org/x/net v0.42.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -385,8 +385,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/gozstd v1.22.0 h1:y9gANljS8mrehiLWrnLK3B98ITnLneIO6FiK2jpvNUo=
|
||||
github.com/valyala/gozstd v1.22.0/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||
github.com/valyala/gozstd v1.23.2 h1:S3rRsskaDvBCM2XJzQFYIDAO6txxmvTc1arA/9Wgi9o=
|
||||
github.com/valyala/gozstd v1.23.2/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
||||
github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdqCysW5k=
|
||||
|
||||
121
lib/configwatcher/watcher.go
Normal file
121
lib/configwatcher/watcher.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package configwatcher
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
)
|
||||
|
||||
// TBD: migrate /-/reload handler to the package
|
||||
// TBD: print registered configs in 10s after the start
|
||||
// TBD: print what reload methods enabled
|
||||
|
||||
type handler struct {
|
||||
flag string
|
||||
handler func()
|
||||
}
|
||||
|
||||
var configCheckInterval = flag.Duration("configCheckInterval", 0, "TBD")
|
||||
|
||||
var signalHandlers []handler
|
||||
|
||||
var checkIntervalHandlers []handler
|
||||
|
||||
var mux = sync.Mutex{}
|
||||
|
||||
func RegisterHandler(flag string, handlerFn func()) {
|
||||
RegisterSignalHandler(flag, handlerFn)
|
||||
RegisterCheckIntervalHandler(flag, handlerFn)
|
||||
}
|
||||
|
||||
func RegisterSignalHandler(flag string, handlerFn func()) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
signalHandlers = append(signalHandlers, handler{
|
||||
flag: flag,
|
||||
handler: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
func RegisterCheckIntervalHandler(flag string, handlerFn func()) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
checkIntervalHandlers = append(checkIntervalHandlers, handler{
|
||||
flag: flag,
|
||||
handler: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
func UnregisterHandler(flag string) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
newCheckIntervalHandlers := make([]handler, 0, len(checkIntervalHandlers))
|
||||
for _, h := range checkIntervalHandlers {
|
||||
if h.flag != flag {
|
||||
newCheckIntervalHandlers = append(newCheckIntervalHandlers, h)
|
||||
}
|
||||
}
|
||||
newSignalHandlers := make([]handler, 0, len(signalHandlers))
|
||||
for _, h := range signalHandlers {
|
||||
if h.flag != flag {
|
||||
newSignalHandlers = append(newSignalHandlers, h)
|
||||
}
|
||||
}
|
||||
|
||||
checkIntervalHandlers = newCheckIntervalHandlers
|
||||
}
|
||||
|
||||
var stopChan chan struct{}
|
||||
|
||||
func Init() {
|
||||
stopChan = make(chan struct{})
|
||||
go func() {
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
|
||||
var tickerCh <-chan time.Time
|
||||
if *configCheckInterval > 0 {
|
||||
ticker := time.NewTicker(*configCheckInterval)
|
||||
tickerCh = ticker.C
|
||||
defer ticker.Stop()
|
||||
}
|
||||
for {
|
||||
|
||||
select {
|
||||
case <-sighupCh:
|
||||
mux.Lock()
|
||||
for _, h := range signalHandlers {
|
||||
h.handler()
|
||||
}
|
||||
mux.Unlock()
|
||||
case <-tickerCh:
|
||||
mux.Lock()
|
||||
for _, h := range checkIntervalHandlers {
|
||||
h.handler()
|
||||
}
|
||||
mux.Unlock()
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Method for BC
|
||||
func EnableCheckInterval(dur time.Duration) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
if dur > *configCheckInterval {
|
||||
*configCheckInterval = dur
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops Prometheus scraper.
|
||||
func Stop() {
|
||||
close(stopChan)
|
||||
}
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
// since the returned value can be put in logs.
|
||||
// Call Password.Get() for obtaining the real password value.
|
||||
func NewPassword(name, description string) *Password {
|
||||
description += fmt.Sprintf("\nFlag value can be read from the given file when using -%s=file:///abs/path/to/file or -%s=file://./relative/path/to/file . "+
|
||||
"Flag value can be read from the given http/https url when using -%s=http://host/path or -%s=https://host/path", name, name, name, name)
|
||||
description += fmt.Sprintf("\nFlag value can be read from the given file when using -%s=file:///abs/path/to/file or -%s=file://./relative/path/to/file."+
|
||||
"\nFlag value can be read from the given http/https url when using -%s=http://host/path or -%s=https://host/path", name, name, name, name)
|
||||
p := &Password{
|
||||
flagname: name,
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/configwatcher"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/auth"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/procutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/prompb"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/azure"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discovery/consul"
|
||||
@@ -113,7 +113,7 @@ func runScraper(configFile string, pushData func(at *auth.Token, wr *prompb.Writ
|
||||
// Register SIGHUP handler for config reload before loadConfig.
|
||||
// This guarantees that the config will be re-read if the signal arrives just after loadConfig.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1240
|
||||
sighupCh := procutil.NewSighupChan()
|
||||
//sighupCh := procutil.NewSighupChan()
|
||||
|
||||
logger.Infof("reading scrape configs from %q", configFile)
|
||||
cfg, err := loadConfig(configFile)
|
||||
@@ -152,61 +152,69 @@ func runScraper(configFile string, pushData func(at *auth.Token, wr *prompb.Writ
|
||||
scs.add("yandexcloud_sd_configs", *yandexcloud.SDCheckInterval, func(cfg *Config, swsPrev []*ScrapeWork) []*ScrapeWork { return cfg.getYandexCloudSDScrapeWork(swsPrev) })
|
||||
scs.add("static_configs", 0, func(cfg *Config, _ []*ScrapeWork) []*ScrapeWork { return cfg.getStaticScrapeWork() })
|
||||
|
||||
var tickerCh <-chan time.Time
|
||||
scs.updateConfig(cfg)
|
||||
|
||||
if *configCheckInterval > 0 {
|
||||
ticker := time.NewTicker(*configCheckInterval)
|
||||
tickerCh = ticker.C
|
||||
defer ticker.Stop()
|
||||
}
|
||||
for {
|
||||
scs.updateConfig(cfg)
|
||||
waitForChans:
|
||||
select {
|
||||
case <-sighupCh:
|
||||
logger.Infof("SIGHUP received; reloading Prometheus configs from %q", configFile)
|
||||
cfgNew, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("cannot read %q on SIGHUP: %s; continuing with the previous config", configFile, err)
|
||||
goto waitForChans
|
||||
}
|
||||
configSuccess.Set(1)
|
||||
if !cfgNew.mustRestart(cfg) {
|
||||
logger.Infof("nothing changed in %q", configFile)
|
||||
goto waitForChans
|
||||
}
|
||||
cfg = cfgNew
|
||||
marshaledData = cfg.marshal()
|
||||
configData.Store(&marshaledData)
|
||||
configReloads.Inc()
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
case <-tickerCh:
|
||||
// TBD print notice that deprecated -promscrape.configCheckInterval is used
|
||||
configwatcher.EnableCheckInterval(*configCheckInterval)
|
||||
|
||||
configwatcher.RegisterCheckIntervalHandler("-promscrape.config", func() {
|
||||
cfgNew, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("cannot read %q: %s; continuing with the previous config", configFile, err)
|
||||
goto waitForChans
|
||||
return
|
||||
}
|
||||
configSuccess.Set(1)
|
||||
if !cfgNew.mustRestart(cfg) {
|
||||
goto waitForChans
|
||||
return
|
||||
}
|
||||
cfg = cfgNew
|
||||
marshaledData = cfg.marshal()
|
||||
configData.Store(&marshaledData)
|
||||
configReloads.Inc()
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
case <-globalStopCh:
|
||||
cfg.mustStop()
|
||||
logger.Infof("stopping Prometheus scrapers")
|
||||
startTime := time.Now()
|
||||
scs.stop()
|
||||
logger.Infof("stopped Prometheus scrapers in %.3f seconds", time.Since(startTime).Seconds())
|
||||
|
||||
scs.updateConfig(cfg)
|
||||
})
|
||||
}
|
||||
|
||||
configwatcher.RegisterSignalHandler("-promscrape.config", func() {
|
||||
logger.Infof("SIGHUP received; reloading Prometheus configs from %q", configFile)
|
||||
cfgNew, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
configReloadErrors.Inc()
|
||||
configSuccess.Set(0)
|
||||
logger.Errorf("cannot read %q on SIGHUP: %s; continuing with the previous config", configFile, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
configSuccess.Set(1)
|
||||
if !cfgNew.mustRestart(cfg) {
|
||||
logger.Infof("nothing changed in %q", configFile)
|
||||
return
|
||||
}
|
||||
cfg = cfgNew
|
||||
marshaledData = cfg.marshal()
|
||||
configData.Store(&marshaledData)
|
||||
configReloads.Inc()
|
||||
configTimestamp.Set(fasttime.UnixTimestamp())
|
||||
|
||||
scs.updateConfig(cfg)
|
||||
})
|
||||
|
||||
go func() {
|
||||
<-globalStopCh
|
||||
|
||||
configwatcher.UnregisterHandler("-promscrape.config")
|
||||
|
||||
cfg.mustStop()
|
||||
logger.Infof("stopping Prometheus scrapers")
|
||||
startTime := time.Now()
|
||||
scs.stop()
|
||||
logger.Infof("stopped Prometheus scrapers in %.3f seconds", time.Since(startTime).Seconds())
|
||||
return
|
||||
}()
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,107 +1,78 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func BenchmarkSearch_VariousTimeRanges(b *testing.B) {
|
||||
f := func(b *testing.B, tr TimeRange) {
|
||||
b.Helper()
|
||||
func BenchmarkSearchData_variableSeries(b *testing.B) {
|
||||
benchmarkSearch_variableSeries(b, false, benchmarkSearchData)
|
||||
}
|
||||
|
||||
const numRows = 10_000
|
||||
want := make([]MetricRow, numRows)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
|
||||
mn := MetricName{
|
||||
Tags: []Tag{
|
||||
{[]byte("job"), []byte("webservice")},
|
||||
{[]byte("instance"), []byte("1.2.3.4")},
|
||||
},
|
||||
}
|
||||
for i := range numRows {
|
||||
name := fmt.Sprintf("metric_%d", i)
|
||||
mn.MetricGroup = []byte(name)
|
||||
want[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
want[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
want[i].Value = float64(i)
|
||||
}
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(want[:numRows/2], defaultPrecisionBits)
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(want[numRows/2:], defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
func BenchmarkSearchData_variableDeletedSeries(b *testing.B) {
|
||||
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchData)
|
||||
}
|
||||
|
||||
tfss := NewTagFilters()
|
||||
if err := tfss.Add(nil, []byte("metric_.*"), false, true); err != nil {
|
||||
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
}
|
||||
func BenchmarkSearchData_variableTimeRange(b *testing.B) {
|
||||
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchData)
|
||||
}
|
||||
|
||||
// Reset timer to exclude expensive initialization from measurement.
|
||||
b.ResetTimer()
|
||||
|
||||
type metricBlock struct {
|
||||
MetricName []byte
|
||||
Block *Block
|
||||
}
|
||||
var mbs []metricBlock
|
||||
for range b.N {
|
||||
mbs = nil
|
||||
var search Search
|
||||
search.Init(nil, s, []*TagFilters{tfss}, tr, 1e9, noDeadline)
|
||||
for search.NextMetricBlock() {
|
||||
var (
|
||||
block Block
|
||||
mb metricBlock
|
||||
)
|
||||
search.MetricBlockRef.BlockRef.MustReadBlock(&block)
|
||||
mb.MetricName = append(mb.MetricName, search.MetricBlockRef.MetricName...)
|
||||
mb.Block = &block
|
||||
mbs = append(mbs, mb)
|
||||
}
|
||||
if err := search.Error(); err != nil {
|
||||
b.Fatalf("search error: %v", err)
|
||||
}
|
||||
search.MustClose()
|
||||
}
|
||||
|
||||
// Stop timer to exclude expensive correctness check and cleanup from
|
||||
// measurement.
|
||||
b.StopTimer()
|
||||
|
||||
var got []MetricRow
|
||||
for _, mb := range mbs {
|
||||
rb := newTestRawBlock(mb.Block, tr)
|
||||
if err := mn.Unmarshal(mb.MetricName); err != nil {
|
||||
b.Fatalf("cannot unmarshal MetricName %v: %v", string(mb.MetricName), err)
|
||||
}
|
||||
metricNameRaw := mn.marshalRaw(nil)
|
||||
for i, timestamp := range rb.Timestamps {
|
||||
mr := MetricRow{
|
||||
MetricNameRaw: metricNameRaw,
|
||||
Timestamp: timestamp,
|
||||
Value: rb.Values[i],
|
||||
}
|
||||
got = append(got, mr)
|
||||
}
|
||||
}
|
||||
|
||||
testSortMetricRows(got)
|
||||
testSortMetricRows(want)
|
||||
if diff := cmp.Diff(mrsToString(want), mrsToString(got)); diff != "" {
|
||||
b.Errorf("unexpected metric names (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(b.Name())
|
||||
|
||||
// Start timer again to conclude the benchmark correctly.
|
||||
b.StartTimer()
|
||||
// benchmarkSearchData is a helper function used in various data search
|
||||
// benchmarks.
|
||||
func benchmarkSearchData(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
|
||||
b.Helper()
|
||||
tfss := NewTagFilters()
|
||||
if err := tfss.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
}
|
||||
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
type metricBlock struct {
|
||||
MetricName []byte
|
||||
Block *Block
|
||||
}
|
||||
mbs := make([]metricBlock, 0, len(mrs))
|
||||
for b.Loop() {
|
||||
mbs = mbs[:0]
|
||||
var search Search
|
||||
search.Init(nil, s, []*TagFilters{tfss}, tr, 1e9, noDeadline)
|
||||
for search.NextMetricBlock() {
|
||||
var (
|
||||
block Block
|
||||
mb metricBlock
|
||||
)
|
||||
search.MetricBlockRef.BlockRef.MustReadBlock(&block)
|
||||
mb.MetricName = append(mb.MetricName, search.MetricBlockRef.MetricName...)
|
||||
mb.Block = &block
|
||||
mbs = append(mbs, mb)
|
||||
}
|
||||
if err := search.Error(); err != nil {
|
||||
b.Fatalf("search error: %v", err)
|
||||
}
|
||||
search.MustClose()
|
||||
}
|
||||
|
||||
var mn MetricName
|
||||
got := make([]MetricRow, len(mrs))
|
||||
for i, mb := range mbs {
|
||||
rb := newTestRawBlock(mb.Block, tr)
|
||||
if err := mn.Unmarshal(mb.MetricName); err != nil {
|
||||
b.Fatalf("cannot unmarshal MetricName %v: %v", string(mb.MetricName), err)
|
||||
}
|
||||
metricNameRaw := mn.marshalRaw(nil)
|
||||
for j, timestamp := range rb.Timestamps {
|
||||
mr := MetricRow{
|
||||
MetricNameRaw: metricNameRaw,
|
||||
Timestamp: timestamp,
|
||||
Value: rb.Values[j],
|
||||
}
|
||||
got[i] = mr
|
||||
}
|
||||
}
|
||||
testSortMetricRows(got)
|
||||
want := mrs
|
||||
testSortMetricRows(want)
|
||||
if diff := cmp.Diff(mrsToString(want), mrsToString(got)); diff != "" {
|
||||
b.Errorf("unexpected metric rows (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -101,331 +102,6 @@ func BenchmarkStorageAddRows_VariousTimeRanges(b *testing.B) {
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
}
|
||||
|
||||
func BenchmarkStorageSearchMetricNames_VariousTimeRanges(b *testing.B) {
|
||||
f := func(b *testing.B, tr TimeRange) {
|
||||
b.Helper()
|
||||
|
||||
const numRows = 10_000
|
||||
mrs := make([]MetricRow, numRows)
|
||||
want := make([]string, numRows)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
|
||||
mn := MetricName{
|
||||
Tags: []Tag{
|
||||
{[]byte("job"), []byte("webservice")},
|
||||
{[]byte("instance"), []byte("1.2.3.4")},
|
||||
},
|
||||
}
|
||||
for i := range numRows {
|
||||
name := fmt.Sprintf("metric_%d", i)
|
||||
mn.MetricGroup = []byte(name)
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
want[i] = name
|
||||
}
|
||||
slices.Sort(want)
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(mrs[:numRows/2], defaultPrecisionBits)
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(mrs[numRows/2:], defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
tfss := NewTagFilters()
|
||||
if err := tfss.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
}
|
||||
|
||||
// Reset timer to exclude expensive initialization from measurement.
|
||||
b.ResetTimer()
|
||||
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for range b.N {
|
||||
got, err = s.SearchMetricNames(nil, []*TagFilters{tfss}, tr, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timer to exclude expensive correctness check and cleanup from
|
||||
// measurement.
|
||||
b.StopTimer()
|
||||
|
||||
for i, name := range got {
|
||||
var mn MetricName
|
||||
if err := mn.UnmarshalString(name); err != nil {
|
||||
b.Fatalf("Could not unmarshal metric name %q: %v", name, err)
|
||||
}
|
||||
got[i] = string(mn.MetricGroup)
|
||||
}
|
||||
slices.Sort(got)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Errorf("unexpected metric names (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(b.Name())
|
||||
|
||||
// Start timer again to conclude the benchmark correctly.
|
||||
b.StartTimer()
|
||||
}
|
||||
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
}
|
||||
|
||||
func BenchmarkStorageSearchLabelNames_VariousTimeRanges(b *testing.B) {
|
||||
f := func(b *testing.B, tr TimeRange) {
|
||||
b.Helper()
|
||||
|
||||
const numRows = 10_000
|
||||
mrs := make([]MetricRow, numRows)
|
||||
want := make([]string, numRows)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
|
||||
mn := MetricName{
|
||||
MetricGroup: []byte("metric"),
|
||||
Tags: []Tag{
|
||||
{
|
||||
Key: []byte("tbd"),
|
||||
Value: []byte("value"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for i := range numRows {
|
||||
labelName := fmt.Sprintf("label_%d", i)
|
||||
mn.Tags[0].Key = []byte(labelName)
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
want[i] = labelName
|
||||
}
|
||||
want = append(want, "__name__")
|
||||
slices.Sort(want)
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(mrs[:numRows/2], defaultPrecisionBits)
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(mrs[numRows/2:], defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
// Reset timer to exclude expensive initialization from measurement.
|
||||
b.ResetTimer()
|
||||
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
|
||||
for range b.N {
|
||||
got, err = s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchLabelNames() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timer to exclude expensive correctness check and cleanup from
|
||||
// measurement.
|
||||
b.StopTimer()
|
||||
|
||||
slices.Sort(got)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Errorf("unexpected label names (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(b.Name())
|
||||
|
||||
// Start timer again to conclude the benchmark correctly.
|
||||
b.StartTimer()
|
||||
}
|
||||
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
}
|
||||
|
||||
func BenchmarkStorageSearchLabelValues_VariousTimeRanges(b *testing.B) {
|
||||
f := func(b *testing.B, tr TimeRange) {
|
||||
b.Helper()
|
||||
|
||||
const numRows = 10_000
|
||||
mrs := make([]MetricRow, numRows)
|
||||
want := make([]string, numRows)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
|
||||
mn := MetricName{
|
||||
MetricGroup: []byte("metric"),
|
||||
Tags: []Tag{
|
||||
{
|
||||
Key: []byte("label"),
|
||||
Value: []byte("tbd"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for i := range numRows {
|
||||
labelValue := fmt.Sprintf("value_%d", i)
|
||||
mn.Tags[0].Value = []byte(labelValue)
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
want[i] = labelValue
|
||||
}
|
||||
slices.Sort(want)
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(mrs[:numRows/2], defaultPrecisionBits)
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(mrs[numRows/2:], defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
// Reset timer to exclude expensive initialization from measurement.
|
||||
b.ResetTimer()
|
||||
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for range b.N {
|
||||
got, err = s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchLabelValues() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timer to exclude expensive correctness check and cleanup from
|
||||
// measurement.
|
||||
b.StopTimer()
|
||||
|
||||
slices.Sort(got)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Errorf("unexpected label values (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(b.Name())
|
||||
|
||||
// Start timer again to conclude the benchmark correctly.
|
||||
b.StartTimer()
|
||||
}
|
||||
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
}
|
||||
|
||||
func BenchmarkStorageSearchTagValueSuffixes_VariousTimeRanges(b *testing.B) {
|
||||
f := func(b *testing.B, tr TimeRange) {
|
||||
b.Helper()
|
||||
|
||||
const numMetrics = 10_000
|
||||
mrs := make([]MetricRow, numMetrics)
|
||||
want := make([]string, numMetrics)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numMetrics)
|
||||
for i := range numMetrics {
|
||||
name := fmt.Sprintf("prefix.metric%04d", i)
|
||||
mn := MetricName{MetricGroup: []byte(name)}
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
want[i] = fmt.Sprintf("metric%04d", i)
|
||||
}
|
||||
slices.Sort(want)
|
||||
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(mrs[:numMetrics/2], defaultPrecisionBits)
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(mrs[numMetrics/2:], defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
// Reset timer to exclude expensive initialization from measurement.
|
||||
b.ResetTimer()
|
||||
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for range b.N {
|
||||
got, err = s.SearchTagValueSuffixes(nil, tr, "", "prefix.", '.', 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchTagValueSuffixes() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timer to exclude expensive correctness check and cleanup from
|
||||
// measurement.
|
||||
b.StopTimer()
|
||||
|
||||
slices.Sort(got)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected tag value suffixes (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(b.Name())
|
||||
|
||||
// Start timer again to conclude the benchmark correctly.
|
||||
b.StartTimer()
|
||||
}
|
||||
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
}
|
||||
|
||||
func BenchmarkStorageSearchGraphitePaths_VariousTimeRanges(b *testing.B) {
|
||||
f := func(b *testing.B, tr TimeRange) {
|
||||
b.Helper()
|
||||
|
||||
const numMetrics = 10_000
|
||||
mrs := make([]MetricRow, numMetrics)
|
||||
want := make([]string, numMetrics)
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numMetrics)
|
||||
for i := range numMetrics {
|
||||
name := fmt.Sprintf("prefix.metric%04d", i)
|
||||
mn := MetricName{MetricGroup: []byte(name)}
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
want[i] = name
|
||||
}
|
||||
slices.Sort(want)
|
||||
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(mrs[:numMetrics/2], defaultPrecisionBits)
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(mrs[numMetrics/2:], defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
// Reset timer to exclude expensive initialization from measurement.
|
||||
b.ResetTimer()
|
||||
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for range b.N {
|
||||
got, err = s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchGraphitePaths() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop timer to exclude expensive correctness check and cleanup from
|
||||
// measurement.
|
||||
b.StopTimer()
|
||||
|
||||
slices.Sort(got)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected graphite paths (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
s.MustClose()
|
||||
fs.MustRemoveDir(b.Name())
|
||||
|
||||
// Start timer again to conclude the benchmark correctly.
|
||||
b.StartTimer()
|
||||
}
|
||||
|
||||
benchmarkStorageOpOnVariousTimeRanges(b, f)
|
||||
}
|
||||
|
||||
// benchmarkStorageOpOnVariousTimeRanges measures the execution time of some
|
||||
// storage operation on various time ranges: 1h, 1d, 1m, etc.
|
||||
func benchmarkStorageOpOnVariousTimeRanges(b *testing.B, op func(b *testing.B, tr TimeRange)) {
|
||||
@@ -612,3 +288,416 @@ func benchmarkDirSize(path string) int64 {
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func BenchmarkSearchMetricNames_variableSeries(b *testing.B) {
|
||||
benchmarkSearch_variableSeries(b, false, benchmarkSearchMetricNames)
|
||||
}
|
||||
|
||||
func BenchmarkSearchMetricNames_variableDeletedSeries(b *testing.B) {
|
||||
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchMetricNames)
|
||||
}
|
||||
|
||||
func BenchmarkSearchMetricNames_variableTimeRange(b *testing.B) {
|
||||
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchMetricNames)
|
||||
}
|
||||
|
||||
func BenchmarkSearchLabelNames_variableSeries(b *testing.B) {
|
||||
benchmarkSearch_variableSeries(b, false, benchmarkSearchLabelNames)
|
||||
}
|
||||
|
||||
func BenchmarkSearchLabelNames_variableTimeRange(b *testing.B) {
|
||||
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchLabelNames)
|
||||
}
|
||||
|
||||
func BenchmarkSearchLabelNames_variableDeletedSeries(b *testing.B) {
|
||||
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchLabelNames)
|
||||
}
|
||||
|
||||
func BenchmarkSearchLabelValues_variableSeries(b *testing.B) {
|
||||
benchmarkSearch_variableSeries(b, false, benchmarkSearchLabelValues)
|
||||
}
|
||||
|
||||
func BenchmarkSearchLabelValues_variableDeletedSeries(b *testing.B) {
|
||||
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchLabelValues)
|
||||
}
|
||||
|
||||
func BenchmarkSearchLabelValues_variableTimeRange(b *testing.B) {
|
||||
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchLabelValues)
|
||||
}
|
||||
|
||||
func BenchmarkSearchTagValueSuffixes_variableSeries(b *testing.B) {
|
||||
benchmarkSearch_variableSeries(b, true, benchmarkSearchTagValueSuffixes)
|
||||
}
|
||||
|
||||
func BenchmarkSearchTagValueSuffixes_variableDeletedSeries(b *testing.B) {
|
||||
benchmarkSearch_variableDeletedSeries(b, true, benchmarkSearchTagValueSuffixes)
|
||||
}
|
||||
|
||||
func BenchmarkSearchTagValueSuffixes_variableTimeRange(b *testing.B) {
|
||||
benchmarkSearch_variableTimeRange(b, true, benchmarkSearchTagValueSuffixes)
|
||||
}
|
||||
|
||||
func BenchmarkSearchGraphitePaths_variableSeries(b *testing.B) {
|
||||
benchmarkSearch_variableSeries(b, true, benchmarkSearchGraphitePaths)
|
||||
}
|
||||
|
||||
func BenchmarkSearchGraphitePaths_variableDeletedSeries(b *testing.B) {
|
||||
benchmarkSearch_variableDeletedSeries(b, true, benchmarkSearchGraphitePaths)
|
||||
}
|
||||
|
||||
func BenchmarkSearchGraphitePaths_variableTimeRange(b *testing.B) {
|
||||
benchmarkSearch_variableTimeRange(b, true, benchmarkSearchGraphitePaths)
|
||||
}
|
||||
|
||||
// benchmarkSearch_variableSeries measures the execution time of some search
|
||||
// operation on a fixed time trange and variable number of series. The number of
|
||||
// deleted series is 0.
|
||||
func benchmarkSearch_variableSeries(b *testing.B, graphite bool, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
|
||||
const numDeletedSeries = 0
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
// Using only a few numbers that represent orders of magnitude so that
|
||||
// routine running of the benchmarks does not take too long. However, when
|
||||
// debugging it is often helpful to add more numbers in between these
|
||||
// numbers.
|
||||
for _, numSeries := range []int{100, 1000, 10_000, 100_000, 1_000_000} {
|
||||
name := fmt.Sprintf("%d", numSeries)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
benchmarkSearch(b, graphite, numSeries, numDeletedSeries, tr, op)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearch_variableDeletedSeries measures the execution time of some
|
||||
// storage operation on a fixed time, fixed number of series and variable
|
||||
// number of deleted series.
|
||||
func benchmarkSearch_variableDeletedSeries(b *testing.B, graphite bool, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
|
||||
// Deployments that we aware of often have tens and hundreds of thouthands
|
||||
// series in their query results, sometimes even millions. Chosen 100K as
|
||||
// something in the middle.
|
||||
const numSeries = 100_000
|
||||
tr := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
for _, numDeletedSeries := range []int{100, 1000, 10_000, 100_000, 1_000_000} {
|
||||
name := fmt.Sprintf("%d", numDeletedSeries)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
benchmarkSearch(b, graphite, numSeries, numDeletedSeries, tr, op)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearch_variableTimeRange measures the execution time of some search
|
||||
// operation on various time trages and fixed number of series. The number of
|
||||
// deleted series is 0.
|
||||
func benchmarkSearch_variableTimeRange(b *testing.B, graphite bool, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
|
||||
const (
|
||||
numSeries = 100_000
|
||||
numDeletedSeries = 0
|
||||
)
|
||||
tr1d := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr1w := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 1, 7, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr1m := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr2m := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 2, 28, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
tr6m := TimeRange{
|
||||
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
MaxTimestamp: time.Date(2025, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
|
||||
}
|
||||
trNames := []string{"1d", "1w", "1m", "2m", "6m"}
|
||||
for i, tr := range []TimeRange{tr1d, tr1w, tr2m, tr1m, tr6m} {
|
||||
name := trNames[i]
|
||||
b.Run(name, func(b *testing.B) {
|
||||
benchmarkSearch(b, graphite, numSeries, numDeletedSeries, tr, op)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearchMetricNames is a helper function used in various
|
||||
// SearchMetricNames benchmarks.
|
||||
func benchmarkSearchMetricNames(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
|
||||
b.Helper()
|
||||
tfss := NewTagFilters()
|
||||
if err := tfss.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
|
||||
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
}
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for b.Loop() {
|
||||
got, err = s.SearchMetricNames(nil, []*TagFilters{tfss}, tr, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var mn MetricName
|
||||
for i, name := range got {
|
||||
if err := mn.UnmarshalString(name); err != nil {
|
||||
b.Fatalf("Could not unmarshal metric name %q: %v", name, err)
|
||||
}
|
||||
got[i] = string(mn.MetricGroup)
|
||||
}
|
||||
slices.Sort(got)
|
||||
want := make([]string, len(mrs))
|
||||
for i, mr := range mrs {
|
||||
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
|
||||
b.Fatalf("could not unmarshal metric row: %v", err)
|
||||
}
|
||||
want[i] = string(mn.MetricGroup)
|
||||
}
|
||||
slices.Sort(want)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearchLabelNames is a helper function used in various
|
||||
// SearchLabelNames benchmarks.
|
||||
func benchmarkSearchLabelNames(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
|
||||
b.Helper()
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for b.Loop() {
|
||||
got, err = s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchLabelNames() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
slices.Sort(got)
|
||||
var mn MetricName
|
||||
want := make([]string, len(mrs))
|
||||
for i, mr := range mrs {
|
||||
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
|
||||
b.Fatalf("could not unmarshal metric row: %v", err)
|
||||
}
|
||||
for _, tag := range mn.Tags {
|
||||
labelName := string(tag.Key)
|
||||
if labelName != "label" {
|
||||
want[i] = labelName
|
||||
}
|
||||
}
|
||||
}
|
||||
want = append(want, "__name__", "label")
|
||||
slices.Sort(want)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected label names (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearchLabelValues is a helper function used in various
|
||||
// SearchLabelValues benchmarks.
|
||||
func benchmarkSearchLabelValues(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
|
||||
b.Helper()
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for b.Loop() {
|
||||
got, err = s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchLabelValues() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
slices.Sort(got)
|
||||
want := make([]string, len(mrs))
|
||||
for i, mr := range mrs {
|
||||
var mn MetricName
|
||||
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
|
||||
b.Fatalf("could not unmarshal metric row: %v", err)
|
||||
}
|
||||
for _, tag := range mn.Tags {
|
||||
if string(tag.Key) == "label" {
|
||||
want[i] = string(tag.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.Sort(want)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected label values (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearchTagValueSuffixes is a helper function used in various
|
||||
// SearchTagValueSuffixes benchmarks.
|
||||
func benchmarkSearchTagValueSuffixes(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
|
||||
b.Helper()
|
||||
var (
|
||||
prefix = "graphite."
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for b.Loop() {
|
||||
got, err = s.SearchTagValueSuffixes(nil, tr, "", prefix, '.', 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchTagValueSuffixes() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
slices.Sort(got)
|
||||
want := make([]string, len(mrs))
|
||||
for i, mr := range mrs {
|
||||
var mn MetricName
|
||||
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
|
||||
b.Fatalf("could not unmarshal metric row: %v", err)
|
||||
}
|
||||
var found bool
|
||||
metricName := string(mn.MetricGroup)
|
||||
want[i], found = strings.CutPrefix(metricName, prefix)
|
||||
if !found {
|
||||
b.Fatalf("metric name %q does not have %q prefix", metricName, prefix)
|
||||
}
|
||||
}
|
||||
slices.Sort(want)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected tag value suffixes (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearchGraphitePaths is a helper function used in various
|
||||
// SearchGraphitePaths benchmarks.
|
||||
func benchmarkSearchGraphitePaths(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
|
||||
b.Helper()
|
||||
var (
|
||||
got []string
|
||||
err error
|
||||
)
|
||||
for b.Loop() {
|
||||
got, err = s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
|
||||
if err != nil {
|
||||
b.Fatalf("SearchGraphitePaths() failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
slices.Sort(got)
|
||||
want := make([]string, len(mrs))
|
||||
for i, mr := range mrs {
|
||||
var mn MetricName
|
||||
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
|
||||
b.Fatalf("could not unmarshal metric row: %v", err)
|
||||
}
|
||||
want[i] = string(mn.MetricGroup)
|
||||
}
|
||||
slices.Sort(want)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
b.Fatalf("unexpected graphite paths (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkSearch implements the core logic of benchmark of a search operation.
|
||||
//
|
||||
// It generates the test data, inserts it into the storage and runs the search
|
||||
// operation against it. The index data is split evenly across prev and curr
|
||||
// indexDBs.
|
||||
//
|
||||
// The number of series is controlled with numSeries.
|
||||
//
|
||||
// The function also generates the deleted series and saves them to the storage.
|
||||
// If the deleted series are not needed, set numDeletedSeries to 0.
|
||||
//
|
||||
// The data is spread evenly across the provided time range.
|
||||
//
|
||||
// The test data is designed so that it can be reused by all types of search
|
||||
// operations. It is also passes to the search op callback to that the search
|
||||
// operation could perform all necessary assertions to make sure that the search
|
||||
// result is correct.
|
||||
func benchmarkSearch(b *testing.B, graphite bool, numSeries, numDeletedSeries int, tr TimeRange, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
|
||||
b.Helper()
|
||||
graphitePrefix := ""
|
||||
if graphite {
|
||||
graphitePrefix = "graphite."
|
||||
}
|
||||
genRows := func(n int, prefix string, tr TimeRange) []MetricRow {
|
||||
mrs := make([]MetricRow, n)
|
||||
if n == 0 {
|
||||
return mrs
|
||||
}
|
||||
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(n)
|
||||
for i := range n {
|
||||
name := fmt.Sprintf("%s%s_%09d", graphitePrefix, prefix, i)
|
||||
labelName := fmt.Sprintf("%s_label_%09d", prefix, i)
|
||||
labelValue := fmt.Sprintf("%s_value_%09d", prefix, i)
|
||||
mn := MetricName{
|
||||
MetricGroup: []byte(name),
|
||||
Tags: []Tag{
|
||||
{[]byte(labelName), []byte("value")},
|
||||
{[]byte("label"), []byte(labelValue)},
|
||||
},
|
||||
}
|
||||
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
|
||||
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
|
||||
mrs[i].Value = float64(i)
|
||||
}
|
||||
return mrs
|
||||
}
|
||||
|
||||
deleteSeries := func(s *Storage, prefix string, want int) {
|
||||
b.Helper()
|
||||
tfs := NewTagFilters()
|
||||
re := fmt.Sprintf(`%s%s.*`, graphitePrefix, prefix)
|
||||
if err := tfs.Add(nil, []byte(re), false, true); err != nil {
|
||||
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
|
||||
}
|
||||
got, err := s.DeleteSeries(nil, []*TagFilters{tfs}, 1e9)
|
||||
if err != nil {
|
||||
b.Fatalf("could not delete series unexpectedly: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
b.Fatalf("unexpected number of deleted series: got %d, want %d", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trPrev := TimeRange{
|
||||
MinTimestamp: tr.MinTimestamp,
|
||||
MaxTimestamp: tr.MinTimestamp + (tr.MaxTimestamp-tr.MinTimestamp)/2,
|
||||
}
|
||||
trCurr := TimeRange{
|
||||
MinTimestamp: tr.MinTimestamp + (tr.MaxTimestamp-tr.MinTimestamp)/2 + 1,
|
||||
MaxTimestamp: tr.MaxTimestamp,
|
||||
}
|
||||
|
||||
numDeletedSeriesPrev := numDeletedSeries / 2
|
||||
mrsToDeletePrev := genRows(numDeletedSeriesPrev, "prev", trPrev)
|
||||
mrsPrev := genRows(numSeries/2, "prev", trPrev)
|
||||
numDeletedSeriesCurr := numDeletedSeries / 2
|
||||
mrsToDeleteCurr := genRows(numDeletedSeriesCurr, "curr", trCurr)
|
||||
mrsCurr := genRows(numSeries/2, "curr", trCurr)
|
||||
|
||||
s := MustOpenStorage(b.Name(), OpenOptions{})
|
||||
s.AddRows(mrsToDeletePrev, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
deleteSeries(s, "prev", numDeletedSeriesPrev)
|
||||
s.DebugFlush()
|
||||
s.AddRows(mrsPrev, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
|
||||
s.mustRotateIndexDB(time.Now())
|
||||
s.AddRows(mrsToDeleteCurr, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
deleteSeries(s, "curr", numDeletedSeriesCurr)
|
||||
s.DebugFlush()
|
||||
s.AddRows(mrsCurr, defaultPrecisionBits)
|
||||
s.DebugFlush()
|
||||
|
||||
mrs := slices.Concat(mrsPrev, mrsCurr)
|
||||
op(b, s, tr, mrs)
|
||||
|
||||
s.MustClose()
|
||||
_ = os.RemoveAll(b.Name())
|
||||
}
|
||||
|
||||
19
vendor/github.com/valyala/gozstd/Makefile
generated
vendored
19
vendor/github.com/valyala/gozstd/Makefile
generated
vendored
@@ -4,9 +4,9 @@ GOOS_GOARCH := $(GOOS)_$(GOARCH)
|
||||
GOOS_GOARCH_NATIVE := $(shell go env GOHOSTOS)_$(shell go env GOHOSTARCH)
|
||||
LIBZSTD_NAME := libzstd_$(GOOS_GOARCH).a
|
||||
ZSTD_VERSION ?= v1.5.7
|
||||
ZIG_BUILDER_IMAGE=euantorano/zig:0.10.1
|
||||
BUILDER_IMAGE := local/builder_musl:2.0.0-$(shell echo $(ZIG_BUILDER_IMAGE) | tr : _ | tr / _)-1
|
||||
|
||||
ZIG_VERSION ?= 0.15.1
|
||||
BASE_IMAGE ?= alpine:latest
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(BASE_IMAGE) | tr : _ | tr / _)-zig-$(ZIG_VERSION)
|
||||
.PHONY: libzstd.a $(LIBZSTD_NAME)
|
||||
|
||||
libzstd.a: $(LIBZSTD_NAME)
|
||||
@@ -32,13 +32,16 @@ else ifeq ($(GOOS_GOARCH),darwin_arm64)
|
||||
else ifeq ($(GOOS_GOARCH),darwin_amd64)
|
||||
TARGET=x86_64-macos GOARCH=amd64 GOOS=darwin $(MAKE) package-arch
|
||||
else ifeq ($(GOOS_GOARCH),windows_amd64)
|
||||
TARGET=x86_64-windows GOARCH=amd64 GOOS=windows GOARCH=amd64 $(MAKE) package-arch
|
||||
TARGET=x86_64-windows GOARCH=amd64 GOOS=windows $(MAKE) package-arch
|
||||
else ifeq ($(GOOS_GOARCH),freebsd_amd64)
|
||||
TARGET=x86_64-freebsd GOARCH=amd64 GOOS=freebsd $(MAKE) package-arch
|
||||
endif
|
||||
|
||||
package-builder:
|
||||
(docker image ls --format '{{.Repository}}:{{.Tag}}' | grep -q '$(BUILDER_IMAGE)$$') \
|
||||
|| docker build \
|
||||
--build-arg builder_image=$(ZIG_BUILDER_IMAGE) \
|
||||
--build-arg builder_image=$(BASE_IMAGE) \
|
||||
--build-arg zig_version=$(ZIG_VERSION) \
|
||||
--tag $(BUILDER_IMAGE) \
|
||||
builder
|
||||
|
||||
@@ -54,10 +57,11 @@ package-arch: package-builder
|
||||
CC="zig cc -target $(TARGET)" \
|
||||
CXX="zig cc -target $(TARGET)" \
|
||||
MOREFLAGS=$(MOREFLAGS) \
|
||||
$(MAKE) clean libzstd.a'
|
||||
RM="rm -rf --" \
|
||||
make clean libzstd.a'
|
||||
mv -f zstd/lib/libzstd.a $(LIBZSTD_NAME)
|
||||
|
||||
# freebsd and illumos aren't supported by zig compiler atm.
|
||||
# illumos isn't supported by zig compiler atm.
|
||||
release:
|
||||
GOOS=linux GOARCH=amd64 $(MAKE) libzstd.a
|
||||
GOOS=linux GOARCH=arm64 $(MAKE) libzstd.a
|
||||
@@ -68,6 +72,7 @@ release:
|
||||
GOOS=darwin GOARCH=arm64 $(MAKE) libzstd.a
|
||||
GOOS=darwin GOARCH=amd64 $(MAKE) libzstd.a
|
||||
GOOS=windows GOARCH=amd64 $(MAKE) libzstd.a
|
||||
GOOS=freebsd GOARCH=amd64 $(MAKE) libzstd.a
|
||||
|
||||
clean:
|
||||
rm -f $(LIBZSTD_NAME)
|
||||
|
||||
BIN
vendor/github.com/valyala/gozstd/libzstd_darwin_amd64.a
generated
vendored
BIN
vendor/github.com/valyala/gozstd/libzstd_darwin_amd64.a
generated
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user