Compare commits

...

19 Commits

Author SHA1 Message Date
Max Kotliar
d0625bb77d wip 2025-08-27 23:31:37 +03:00
Max Kotliar
4163f18250 lib/configwatcher: Introduce a library responsible for configs reloading 2025-08-27 23:28:02 +03:00
Max Kotliar
686289c02b lib/flagutil: fix flag description. 2025-08-27 20:08:24 +03:00
Max Kotliar
9ae10247bb Revert "docs: sync documented flags with binaries"
This reverts commit 7c0c8cc702.
2025-08-27 19:10:31 +03:00
Aliaksandr Valialkin
06ce3f1496 go.mod: update github.com/valyala/gozstd from v1.22.0 to v1.23.2 2025-08-27 14:28:44 +02:00
Artem Fetishev
d0690ba15f benchmarks: support for all query types in TSBS (#9630)
### Describe Your Changes

Add the support of all standard TSDB query types that can be executed
against VictoriaMetrics. `double-groupby-all` is commented out as it
attempts to retrieve all 1B samples and fails. While this can be fixed
by setting the `-search.maxSamplesPerQuery` this query is left disabled
anyway because it will consume way too much memory and cpu time.

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
- [x] My change adheres to [VictoriaMetrics development
goals](https://docs.victoriametrics.com/victoriametrics/goals/).

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-08-27 13:49:35 +02:00
Andrii Chubatiuk
483e00ffb9 vmui: replace VMAlert proxy with Alerting tab in VMUI (#8989)
### Describe Your Changes

Rules page header + content
<img width="1235" height="520" alt="image"
src="https://github.com/user-attachments/assets/bb0c5818-c44a-46e6-bc47-e6718be34016"
/>
Expanded rule without alert
<img width="1418" alt="image"
src="https://github.com/user-attachments/assets/ae0b265f-24fe-4549-8913-b1be8e7c2862"
/>
Expanded rule with alert
<img width="1418" alt="image"
src="https://github.com/user-attachments/assets/8a138403-0712-4de2-bfa5-467da3a979dd"
/>
Notifiers page
<img width="1419" alt="image"
src="https://github.com/user-attachments/assets/557c2831-e960-44ec-9b93-f1ebfeb1fbb0"
/>

fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8330
fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6091
fixes https://github.com/VictoriaMetrics/VictoriaLogs/issues/90

VMUI:
- Added added `Alerting -> Rules` and `Alerting -> Notifiers` pages for
VictoriaMetrics
- Support includeAll option in Select component

VMAlert:
- added `/api/v1/group`useful to get information about certain group
- added `lastError` for `/api/v1/notifiers` for each target to see
information about failed notifiers

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/).

---------

Signed-off-by: hagen1778 <roman@victoriametrics.com>
Co-authored-by: hagen1778 <roman@victoriametrics.com>
2025-08-27 13:48:56 +02:00
Artem Fetishev
06f969a4a7 lib/storage: Follow-up for 9517f5cf1 - use 100k series in all benchmarks, fix benchmark names
Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-08-27 11:53:43 +02:00
Artem Fetishev
9517f5cf1a lib/storage: new storage search benchmarks (#9620)
### Describe Your Changes

New benchmarks for storage search (data and index):
- Use the same dataset that accounts for prev and curr indexDBs and
deleted series
- The code is more structured
- Account for various numbers of series in response including higher
numbers (>10k) as this appears to be a quite common use case.

These bechmarks were used for investigating #9602 performance issue and
helped discover that prefetching metric names needed to be restored
#9619.

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
2025-08-27 11:19:29 +02:00
Alexander Frolov
e62e0685dc vmctl: inconsistent vm-native logs (#9607)
### Describe Your Changes

Some messages were written to `stdout` using `fmt.Printf` and
`fmt.Println`, while the other messages like import statistics were
written to `stderr` through the `log` package.

This led to ordering problems where the `Import finished!` +
`VictoriaMetrics importer stats` messages, which expected to be the last
messages, appeared before `Continue import process with filter`
messages, creating confusing output for users.

```
2025/08/20 13:07:26 Import finished!
2025/08/20 13:07:26 VictoriaMetrics importer stats:
  time spent while importing: 20h49m10.8497184s;
  total bytes: 277.1 GB;
  bytes/s: 3.7 MB;
  requests: 7978614;
  requests retries: 0;
2025/08/20 13:07:26 Total time: 20h49m10.851006088s
Continue import process with filter
        filter: match[]={__name__!=""}
        start: 2025-08-08T00:00:00Z
        end: 2025-08-15T00:00:00Z:
Continue import process with filter
        filter: match[]={__name__!=""}
        start: 2025-08-15T00:00:00Z
        end: 2025-08-19T16:18:15Z:
```


### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
- [x] My change adheres to [VictoriaMetrics development
goals](https://docs.victoriametrics.com/victoriametrics/goals/).
2025-08-26 18:53:59 +03:00
Max Kotliar
df92e617db Revert "app/{vminsert,vmagent}: added flags for periodical relabel and stream aggregation configs check (#9598)"
This reverts commit 07291c1d62 and partly
7c0c8cc702.

The reasons explained in
https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9598#issuecomment-3223766551
2025-08-26 14:42:35 +03:00
Max Kotliar
7c0c8cc702 docs: sync documented flags with binaries 2025-08-26 10:53:43 +03:00
Andrii Chubatiuk
07291c1d62 app/{vminsert,vmagent}: added flags for periodical relabel and stream aggregation configs check (#9598)
related issue
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9590

### Describe Your Changes

Please provide a brief description of the changes you made. Be as
specific as possible to help others understand the purpose and impact of
your modifications.

### Checklist

The following checks are **mandatory**:

- [ ] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
- [ ] My change adheres to [VictoriaMetrics development
goals](https://docs.victoriametrics.com/victoriametrics/goals/).

Co-authored-by: Max Kotliar <mkotlyar@victoriametrics.com>
2025-08-26 09:46:44 +03:00
Alexander Frolov
7c0015b836 app/vmagent/remotewrite: restore protocol downgrade logic (#9621)
### Describe Your Changes

It seems db39f045e1 accidentally reverted
#9419 changes.
```patch
--- a/app/vmagent/remotewrite/client.go
+++ b/app/vmagent/remotewrite/client.go
@@ -448,7 +448,8 @@ again:
 	}
 
 	metrics.GetOrCreateCounter(fmt.Sprintf(`vmagent_remotewrite_requests_total{url=%q, status_code="%d"}`, c.sanitizedURL, statusCode)).Inc()
-	if statusCode == 409 {
+	switch statusCode {
+	case 409:
 		logBlockRejected(block, c.sanitizedURL, resp)
 
 		// Just drop block on 409 status code like Prometheus does.
@@ -461,7 +462,13 @@ again:
 		// - Remote Write v2 specification explicitly specifies a `415 Unsupported Media Type` for unsupported encodings.
 		// - Real-world implementations of v1 use both 400 and 415 status codes.
 		// See more in research: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8462#issuecomment-2786918054
-	} else if statusCode == 415 || statusCode == 400 {
+	case 415, 400:
+		if c.canDowngradeVMProto.Swap(false) {
+			logger.Infof("received unsupported media type or bad request from remote storage at %q. Downgrading protocol from VictoriaMetrics to Prometheus remote write for all future requests. "+
+				"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
+			c.useVMProto.Store(false)
+		}
+
 		if encoding.IsZstd(block) {
 			logger.Infof("received unsupported media type or bad request from remote storage at %q. Re-packing the block to Prometheus remote write and retrying."+
 				"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
```

cc @makasim

### Checklist

The following checks are **mandatory**:

- [x] My change adheres to [VictoriaMetrics contributing
guidelines](https://docs.victoriametrics.com/victoriametrics/contributing/#pull-request-checklist).
- [x] My change adheres to [VictoriaMetrics development
goals](https://docs.victoriametrics.com/victoriametrics/goals/).
2025-08-26 09:17:53 +03:00
Hui Wang
06e52a99fd lib/prompb: replace fields hardcoded hex values with their correspond… (#9617)
…ing bitwise operations

fix https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9608
2025-08-26 09:03:36 +03:00
f41gh7
f5840951a4 app/vmagent: pubsub properly handle ingestion error
Previously, if pushBlockPubSub function returned error, vmagent stopped
remote write worker thread assigned for it. Expected behavior for this
scenario is to retry error inside pushBlockPubSub function. It must
return only on vmagent shutdown.

 This commit properly handles this error and prevents from ingestion
stop.
2025-08-24 21:37:30 +02:00
Aliaksandr Valialkin
9ca5a8d0f4 lib/netutil: return tls.Conn from TCPListener.Accept for TLS connections
This is needed because the servers, which may use the TCPListener, such as net/http.Server,
expect to get tls.Conn for TLS connections in order to properly fill various fields such as net/http.Request.TLS.
If the listener returns some other net.Conn, then these fields aren't filled properly,
and this may prevent from the proper mTLS-based authorization and request routing
such as https://docs.victoriametrics.com/victoriametrics/vmauth/#mtls-based-request-routing

Updates https://github.com/VictoriaMetrics/VictoriaLogs/issues/29
2025-08-22 20:25:40 +02:00
Aliaksandr Valialkin
894b22590d docs/victoriametrics/enterprise.md: mention VictoriaLogs enterprise
Updates https://github.com/VictoriaMetrics/VictoriaLogs/issues/120
2025-08-22 18:31:51 +02:00
hagen1778
f85fd161e4 docs: reword -vmalert.proxyURL usage in vmalert
Make it clear that `-vmalert.proxyURL` needs to be applied to
VM single or vmselect.

Signed-off-by: hagen1778 <roman@victoriametrics.com>
2025-08-22 09:49:13 +02:00
113 changed files with 4851 additions and 1151 deletions

View File

@@ -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())
}

View File

@@ -463,12 +463,6 @@ again:
// - Real-world implementations of v1 use both 400 and 415 status codes.
// See more in research: https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8462#issuecomment-2786918054
case 415, 400:
if c.canDowngradeVMProto.Swap(false) {
logger.Infof("received unsupported media type or bad request from remote storage at %q. Downgrading protocol from VictoriaMetrics to Prometheus remote write for all future requests. "+
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)
c.useVMProto.Store(false)
}
if encoding.IsZstd(block) {
logger.Infof("received unsupported media type or bad request from remote storage at %q. Re-packing the block to Prometheus remote write and retrying."+
"See https://docs.victoriametrics.com/victoriametrics/vmagent/#victoriametrics-remote-write-protocol", c.sanitizedURL)

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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 "" }

View File

@@ -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()
}

View File

@@ -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"

View File

@@ -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)

View File

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

View File

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

View File

@@ -25,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) {

View File

@@ -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

View File

@@ -121,7 +121,7 @@ func (p *vmNativeProcessor) runSingle(ctx context.Context, f native.Filter, srcU
pr := bar.NewProxyReader(reader)
if pr != nil {
reader = pr
fmt.Printf("Continue import process with filter %s:\n", f.String())
fmt.Fprintf(log.Writer(), "Continue import process with filter %s:\n", f.String())
}
}
@@ -191,7 +191,7 @@ func (p *vmNativeProcessor) runBackfilling(ctx context.Context, tenantID string,
initParams = []any{srcURL, dstURL, p.filter.String(), tenantID}
}
fmt.Println("") // extra line for better output formatting
fmt.Fprintln(log.Writer(), "") // extra line for better output formatting
log.Printf(initMessage, initParams...)
if len(ranges) > 1 {
log.Printf("Selected time range will be split into %d ranges according to %q step", len(ranges), p.filter.Chunk)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -36,10 +36,10 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-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>

View File

@@ -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

View File

@@ -1177,7 +1177,7 @@
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"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"

View File

@@ -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;

View File

@@ -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`;

View 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`;
};

View File

@@ -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>
)}
</>
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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(" "));
};

View File

@@ -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>
</>
);
};

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -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}

View 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",
},
];

View File

@@ -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;
}
};

View File

@@ -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",

View File

@@ -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() !== "")

View File

@@ -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 });
}

View File

@@ -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>

View File

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

View File

@@ -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}
/>
);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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 };
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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]);
};

View 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;
}
}
}

View File

@@ -98,7 +98,7 @@ const RawQueryPage: FC = () => {
})}
>
<QueryConfigurator
label={"Time series selector"}
label="Time series selector"
queryErrors={!hideError ? queryErrors : []}
setQueryErrors={setQueryErrors}
setHideError={setHideError}

View File

@@ -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;

View File

@@ -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 },
];
/**

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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";
}
}
}
}
},
},
},
},
};
});

View File

@@ -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

View File

@@ -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**

View File

@@ -26,8 +26,13 @@ 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.
* BUG: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): fix `timestamp` function compatibility with Prometheus when used with sub-expressions such as `timestamp(sum(foo))`. The fix applies only when `-search.disableImplicitConversion` flag is set. See more in [#9527-comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9527#issuecomment-3200646020) and [metricsql#55](https://github.com/VictoriaMetrics/metricsql/pull/55).
* 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).
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/victoriametrics/metricsql/): fix `timestamp` function compatibility with Prometheus when used with sub-expressions such as `timestamp(sum(foo))`. The fix applies only when `-search.disableImplicitConversion` flag is set. See more in [#9527-comment](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9527#issuecomment-3200646020) and [metricsql#55](https://github.com/VictoriaMetrics/metricsql/pull/55).
## [v1.124.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.124.0)
@@ -38,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).

View File

@@ -14,18 +14,26 @@ aliases:
- /enterprise/index.html
- /enterprise/
---
VictoriaMetrics components are provided in two kinds - [Community edition](https://victoriametrics.com/products/open-source/)
and [Enterprise edition](https://victoriametrics.com/products/enterprise/).
VictoriaMetrics community components are open source and are free to use - see [the source code](https://github.com/VictoriaMetrics/VictoriaMetrics/)
and [the license](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE).
VictoriaMetrics and [VictoriaLogs](https://docs.victoriametrics.com/victorialogs/) components are provided
in two kinds - [Community edition](https://victoriametrics.com/products/open-source/) and [Enterprise edition](https://victoriametrics.com/products/enterprise/).
VictoriaMetrics Enterprise components are available in binary form at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
and at [Docker Hub](https://hub.docker.com/u/victoriametrics) and [Quay](https://quay.io/organization/victoriametrics). Enterprise binaries and packages have `enterprise` suffix in their names.
VictoriaMetrics and VictoriaLogs community components are open source and are free to use:
- See [VictoriaMetrics source code](https://github.com/VictoriaMetrics/VictoriaMetrics/) and [VictoriaMetrics license](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/LICENSE).
- See [VictoriaLogs source code](https://github.com/VictoriaMetrics/VictoriaLogs/) and [VictoriaLogs license](https://github.com/VictoriaMetrics/VictoriaLogs/blob/master/LICENSE).
Enterprise components of VictoriaMetrics and VictoriaLogs are available at the following places:
- Binary executables are available at [the releases page for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
and [the release page for VictoriaLogs](https://github.com/VictoriaMetrics/VictoriaLogs/releases/latest).
- Docker images are available at [Docker Hub](https://hub.docker.com/u/victoriametrics) and [Quay](https://quay.io/organization/victoriametrics).
Enterprise executables and Docker images have `enterprise` suffix in their names and tags.
## Valid cases for VictoriaMetrics Enterprise
The use of VictoriaMetrics Enterprise components is permitted in the following cases:
The use of Enterprise components of VictoriaMetrics and VictoriaLogs is permitted in the following cases:
- Evaluation use in non-production setups. Please, request trial license [here](https://victoriametrics.com/products/enterprise/trial/)
and then pass it via `-license` or `-licenseFile` command-line flags as described [in these docs](#running-victoriametrics-enterprise).
@@ -35,7 +43,7 @@ The use of VictoriaMetrics Enterprise components is permitted in the following c
- [VictoriaMetrics Cloud](https://docs.victoriametrics.com/victoriametrics-cloud/) is built on top of VictoriaMetrics Enterprise.
See [these docs](#running-victoriametrics-enterprise) for details on how to run VictoriaMetrics Enterprise.
See [these docs](#running-victoriametrics-enterprise) for details on how to run Enterprise components of VictoriaMetrics and VictoriaLogs.
## VictoriaMetrics Enterprise features
@@ -75,9 +83,28 @@ On top of this, Enterprise package of VictoriaMetrics includes the following fea
Contact us via [this page](https://victoriametrics.com/products/enterprise/) if you are interested in VictoriaMetrics Enterprise.
## VictoriaLogs Enterprise features
VictoriaLogs enterprise includes [all the features of the community edition](https://docs.victoriametrics.com/victorialogs/),
plus the following additional features:
- First-class consulting and technical support provided by the core VictoriaMetrics dev team.
- [Monitoring of monitoring](https://victoriametrics.com/products/mom/) - this feature allows forecasting
and preventing possible issues in VictoriaMetrics setups.
- [Enterprise security compliance](https://victoriametrics.com/security/).
- Prioritizing of feature requests from Enterprise customers.
On top of this, Enterprise package of VictoriaLogs includes the following features:
- [Automatic issuing of TLS certificates](https://docs.victoriametrics.com/victorialogs/#automatic-issuing-of-tls-certificates).
- [mTLS for all the VictoriaMetrics components](https://docs.victoriametrics.com/victorialogs/#mtls).
- [mTLS for communications between cluster components](https://docs.victoriametrics.com/victorialogs/cluster/#mtls).
Contact us via [this page](https://victoriametrics.com/products/enterprise/) if you are interested in VictoriaLogs Enterprise.
## Running VictoriaMetrics Enterprise
VictoriaMetrics Enterprise components are available in the following forms:
Enterprise components of VictoriaMetrics and VictoriaLogs are available in the following forms:
- [Binary releases](#binary-releases)
- [Docker images](#docker-images)
@@ -86,15 +113,16 @@ VictoriaMetrics Enterprise components are available in the following forms:
### Binary releases
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
It is allowed to run VictoriaMetrics and VictoriaLogs Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
Binary releases of VictoriaMetrics Enterprise are available [at the releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest).
Binary releases of Enterprise components are available at [the releases page for VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest)
and [the releases page for VictoriaLogs](https://github.com/VictoriaMetrics/VictoriaLogs/releases/latest).
Enterprise binaries and packages have `enterprise` suffix in their names. For example, `victoria-metrics-linux-amd64-v1.124.0-enterprise.tar.gz`.
In order to run binary release of VictoriaMetrics Enterprise component, please download the `*-enterprise.tar.gz` archive for your OS and architecture
from the [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/latest) and unpack it. Then run the unpacked binary.
In order to run binary release of Enterprise component, please download the `*-enterprise.tar.gz` archive for your OS and architecture
from the corresponding releases page and unpack it. Then run the unpacked binary.
All the VictoriaMetrics Enterprise components require specifying the following command-line flags:
All the Enterprise components of VictoriaMetrics and VictoriaLogs require specifying the following command-line flags:
* `-license` - this flag accepts VictoriaMetrics Enterprise license key, which can be obtained at [this page](https://victoriametrics.com/products/enterprise/trial/)
* `-licenseFile` - this flag accepts a path to file with VictoriaMetrics Enterprise license key,
@@ -120,9 +148,9 @@ Alternatively, VictoriaMetrics Enterprise license can be stored in the file and
### Docker images
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
It is allowed to run VictoriaMetrics and VictoriaLogs Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
Docker images for VictoriaMetrics Enterprise are available at VictoriaMetrics [Docker Hub](https://hub.docker.com/u/victoriametrics) and [Quay](https://quay.io/organization/victoriametrics).
Docker images for Enterprise components are available at [VictoriaMetrics Docker Hub](https://hub.docker.com/u/victoriametrics) and [VictoriaMetrics Quay](https://quay.io/organization/victoriametrics).
Enterprise docker images have `enterprise` suffix in their names. For example, `victoriametrics/victoria-metrics:v1.124.0-enterprise`.
In order to run Docker image of VictoriaMetrics Enterprise component, it is required to provide the license key via command-line
@@ -165,17 +193,17 @@ The example assumes that the license file is stored at `/vm-license` on the host
### Helm charts
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
It is allowed to run VictoriaMetrics and VictoriaLogs Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
Helm charts for VictoriaMetrics Enterprise components are available [here](https://github.com/VictoriaMetrics/helm-charts).
Helm charts for Enterprise components are available [here](https://github.com/VictoriaMetrics/helm-charts).
In order to run VictoriaMetrics Enterprise helm chart it is required to provide the license key via `license` value in `values.yaml` file
In order to run Enterprise helm chart it is required to provide the license key via `license` value in `values.yaml` file
and adjust the image tag to the Enterprise one as described [here](#docker-images).
Enterprise license key can be obtained at [this page](https://victoriametrics.com/products/enterprise/trial/).
For example, the following `values` file for [VictoriaMetrics single-node chart](https://github.com/VictoriaMetrics/helm-charts/tree/master/charts/victoria-metrics-single)
is used to provide key in plain-text:
is used to provide the license key in plain-text:
```yaml
server:
@@ -186,7 +214,7 @@ license:
key: {BASE64_ENCODED_LICENSE_KEY}
```
In order to provide key via existing secret, the following values file is used:
In order to provide the license key via existing secret, the following values file is used:
```yaml
server:
@@ -220,15 +248,15 @@ Note that license key provided by using secret is mounted in a file. This allows
### Kubernetes operator
It is allowed to run VictoriaMetrics Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
It is allowed to run VictoriaMetrics and VictoriaLogs Enterprise components in [cases listed here](#valid-cases-for-victoriametrics-enterprise).
VictoriaMetrics Enterprise components can be deployed via [VictoriaMetrics operator](https://docs.victoriametrics.com/operator/).
Enterprise components can be deployed via [VictoriaMetrics operator](https://docs.victoriametrics.com/operator/).
In order to use Enterprise components it is required to provide the license key via `license` field and adjust the image tag to the enterprise one.
Enterprise license key can be obtained at [this page](https://victoriametrics.com/products/enterprise/trial/).
For example, the following custom resource for [VictoriaMetrics single-node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/)
is used to provide key in plain-text:
is used to provide the license key in plain-text:
```yaml
apiVersion: operator.victoriametrics.com/v1beta1
@@ -243,7 +271,7 @@ spec:
tag: v1.124.0-enterprise
```
In order to provide key via existing secret, the following custom resource is used:
In order to provide the license key via existing secret, the following custom resource is used:
```yaml
apiVersion: operator.victoriametrics.com/v1beta1
@@ -283,10 +311,10 @@ See full list of CRD specifications [here](https://docs.victoriametrics.com/oper
### FIPS Compatibility
VictoriaMetrics Enterprise supports [FIPS 140-3](https://en.wikipedia.org/wiki/FIPS_140-3) compliant mode starting with version {{% available_from "v1.118.0" %}}, using the [Go FIPS 140-3 Cryptographic Module](https://go.dev/blog/fips140).
This ensures all cryptographic operations use a validated FIPS module.
VictoriaMetrics Enterprise supports [FIPS 140-3](https://en.wikipedia.org/wiki/FIPS_140-3) compliant mode starting with version {{% available_from "v1.118.0" %}},
using the [Go FIPS 140-3 Cryptographic Module](https://go.dev/blog/fips140). This ensures all cryptographic operations use a validated FIPS module.
Builds are available for amd64 and arm64
Builds are available for amd64 and arm64 architectures.
Example archive:
@@ -303,7 +331,7 @@ Example Docker image:
## Monitoring license expiration
All the VictoriaMetrics Enterprise components expose the following metrics at the `/metrics` page:
All the VictoriaMetrics and VictoriaLogs Enterprise components expose the following metrics at the `/metrics` page:
* `vm_license_expires_at` - license expiration date in unix timestamp format
* `vm_license_expires_in_seconds` - the number of seconds left until the license expires

View File

@@ -22,12 +22,11 @@ protocol and require `-remoteWrite.url` to be configured.
`vmalert` is heavily inspired by [Prometheus](https://prometheus.io/docs/alerting/latest/overview/)
implementation and aims to be compatible with its syntax.
A [single-node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert)
or [cluster version](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#vmalert)
of VictoriaMetrics are capable of proxying requests to `vmalert` via `-vmalert.proxyURL` command-line flag.
Use this feature for the following cases:
* for proxying requests from [Grafana Alerting UI](https://grafana.com/docs/grafana/latest/alerting/);
* for accessing `vmalert`'s UI through VictoriaMetrics Web interface.
Configure `-vmalert.proxyURL` on VictoriaMetrics [single-node](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmalert)
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 [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
@@ -741,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
View File

@@ -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
View File

@@ -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=

View 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)
}

View File

@@ -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,
}

View File

@@ -26,12 +26,10 @@ func NewTCPListener(name, addr string, useProxyProtocol bool, tlsConfig *tls.Con
if err != nil {
return nil, err
}
if tlsConfig != nil {
ln = tls.NewListener(ln, tlsConfig)
}
ms := metrics.GetDefaultSet()
tln := &TCPListener{
Listener: ln,
tlsConfig: tlsConfig,
useProxyProtocol: useProxyProtocol,
accepts: ms.NewCounter(fmt.Sprintf(`vm_tcplistener_accepts_total{name=%q, addr=%q}`, name, addr)),
@@ -75,6 +73,8 @@ func GetTCPNetwork() string {
type TCPListener struct {
net.Listener
tlsConfig *tls.Config
accepts *metrics.Counter
acceptErrors *metrics.Counter
@@ -109,6 +109,16 @@ func (ln *TCPListener) Accept() (net.Conn, error) {
Conn: conn,
cm: &ln.cm,
}
return sc, nil
if ln.tlsConfig == nil {
return sc, nil
}
// Make sure we return tls.Conn instead of statConn, since servers, which use this listener,
// such as net/http.Server, assume that the TLS connection must be represented as tls.Conn.
// Otherwise they cannot initialize internal fields such as net/http.Request.TLS.
// This results in non-working mTLS-based authorization.
//
// See https://github.com/VictoriaMetrics/VictoriaLogs/issues/29
return tls.Server(sc, ln.tlsConfig), nil
}
}

View File

@@ -38,13 +38,13 @@ func (m *Sample) marshalToSizedBuffer(dst []byte) (int, error) {
if m.Timestamp != 0 {
i = encodeVarint(dst, i, uint64(m.Timestamp))
i--
dst[i] = 0x10
dst[i] = (2 << 3)
}
if m.Value != 0 {
i -= 8
binary.LittleEndian.PutUint64(dst[i:], uint64(math.Float64bits(float64(m.Value))))
i--
dst[i] = 0x9
dst[i] = (1 << 3) | 1
}
return len(dst) - i, nil
}
@@ -59,7 +59,7 @@ func (m *TimeSeries) marshalToSizedBuffer(dst []byte) (int, error) {
i -= size
i = encodeVarint(dst, i, uint64(size))
i--
dst[i] = 0x12
dst[i] = (2 << 3) | 2
}
for j := len(m.Labels) - 1; j >= 0; j-- {
size, err := m.Labels[j].marshalToSizedBuffer(dst[:i])
@@ -69,7 +69,7 @@ func (m *TimeSeries) marshalToSizedBuffer(dst []byte) (int, error) {
i -= size
i = encodeVarint(dst, i, uint64(size))
i--
dst[i] = 0xa
dst[i] = (1 << 3) | 2
}
return len(dst) - i, nil
}
@@ -81,14 +81,14 @@ func (m *Label) marshalToSizedBuffer(dst []byte) (int, error) {
copy(dst[i:], m.Value)
i = encodeVarint(dst, i, uint64(len(m.Value)))
i--
dst[i] = 0x12
dst[i] = (2 << 3) | 2
}
if len(m.Name) > 0 {
i -= len(m.Name)
copy(dst[i:], m.Name)
i = encodeVarint(dst, i, uint64(len(m.Name)))
i--
dst[i] = 0xa
dst[i] = (1 << 3) | 2
}
return len(dst) - i, nil
}
@@ -144,7 +144,7 @@ func (m *WriteRequest) marshalToSizedBuffer(dst []byte) (int, error) {
i -= size
i = encodeVarint(dst, i, uint64(size))
i--
dst[i] = 0x1a
dst[i] = (3 << 3) | 2
}
for j := len(m.Timeseries) - 1; j >= 0; j-- {
size, err := m.Timeseries[j].marshalToSizedBuffer(dst[:i])
@@ -154,7 +154,7 @@ func (m *WriteRequest) marshalToSizedBuffer(dst []byte) (int, error) {
i -= size
i = encodeVarint(dst, i, uint64(size))
i--
dst[i] = 0xa
dst[i] = (1 << 3) | 2
}
return len(dst) - i, nil
}
@@ -196,36 +196,36 @@ func (m *MetricMetadata) marshalToSizedBuffer(dst []byte) (int, error) {
copy(dst[i:], m.Unit)
i = encodeVarint(dst, i, uint64(len(m.Unit)))
i--
dst[i] = 0x2a
dst[i] = (5 << 3) | 2
}
if len(m.Help) > 0 {
i -= len(m.Help)
copy(dst[i:], m.Help)
i = encodeVarint(dst, i, uint64(len(m.Help)))
i--
dst[i] = 0x22
dst[i] = (4 << 3) | 2
}
if len(m.MetricFamilyName) > 0 {
i -= len(m.MetricFamilyName)
copy(dst[i:], m.MetricFamilyName)
i = encodeVarint(dst, i, uint64(len(m.MetricFamilyName)))
i--
dst[i] = 0x12
dst[i] = (2 << 3) | 2
}
if m.Type != 0 {
i = encodeVarint(dst, i, uint64(m.Type))
i--
dst[i] = 0x8
dst[i] = (1 << 3)
}
if m.AccountID != 0 {
i = encodeVarint(dst, i, uint64(m.AccountID))
i--
dst[i] = 0x58
dst[i] = (11 << 3)
}
if m.ProjectID != 0 {
i = encodeVarint(dst, i, uint64(m.ProjectID))
i--
dst[i] = 0x60
dst[i] = (12 << 3)
}
return len(dst) - i, nil
}

View File

@@ -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 (

View File

@@ -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)
}
}

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