mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-23 11:46:40 +03:00
Compare commits
1 Commits
timerpool-
...
vmui/show-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe88c2ca5 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
|
||||
2
.github/workflows/check-licenses.yml
vendored
2
.github/workflows/check-licenses.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
cache: false
|
||||
|
||||
2
.github/workflows/codeql-analysis-go.yml
vendored
2
.github/workflows/codeql-analysis-go.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: Set up Go
|
||||
id: go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: false
|
||||
go-version: stable
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
id: go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
|
||||
11
SECURITY.md
11
SECURITY.md
@@ -4,11 +4,12 @@
|
||||
|
||||
The following versions of VictoriaMetrics receive regular security fixes:
|
||||
|
||||
| Version | Supported |
|
||||
|--------------------------------------------------------------------------------|--------------------|
|
||||
| [Latest release](https://docs.victoriametrics.com/victoriametrics/changelog/) | :white_check_mark: |
|
||||
| [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
|
||||
| other releases | :x: |
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| [latest release](https://docs.victoriametrics.com/victoriametrics/changelog/) | :white_check_mark: |
|
||||
| v1.102.x [LTS line](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
|
||||
| v1.110.x [LTS line](https://docs.victoriametrics.com/victoriametrics/lts-releases/) | :white_check_mark: |
|
||||
| other releases | :x: |
|
||||
|
||||
See [this page](https://victoriametrics.com/security/) for more details.
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ func usage() {
|
||||
const s = `
|
||||
victoria-metrics is a time series database and monitoring solution.
|
||||
|
||||
See the docs at https://docs.victoriametrics.com/victoriametrics/
|
||||
See the docs at https://docs.victoriametrics.com/
|
||||
`
|
||||
flagutil.Usage(s)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ type Group struct {
|
||||
// EvalDelay will adjust the `time` parameter of rule evaluation requests to compensate intentional query delay from datasource.
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5155
|
||||
EvalDelay *promutil.Duration `yaml:"eval_delay,omitempty"`
|
||||
Limit *int `yaml:"limit,omitempty"`
|
||||
Limit int `yaml:"limit,omitempty"`
|
||||
Rules []Rule `yaml:"rules"`
|
||||
Concurrency int `yaml:"concurrency"`
|
||||
// Labels is a set of label value pairs, that will be added to every rule.
|
||||
@@ -91,8 +91,8 @@ func (g *Group) Validate(validateTplFn ValidateTplFn, validateExpressions bool)
|
||||
if g.EvalOffset != nil && g.EvalDelay != nil {
|
||||
return fmt.Errorf("eval_offset cannot be used with eval_delay")
|
||||
}
|
||||
if g.Limit != nil && *g.Limit < 0 {
|
||||
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", *g.Limit)
|
||||
if g.Limit < 0 {
|
||||
return fmt.Errorf("invalid limit %d, shouldn't be less than 0", g.Limit)
|
||||
}
|
||||
if g.Concurrency < 0 {
|
||||
return fmt.Errorf("invalid concurrency %d, shouldn't be less than 0", g.Concurrency)
|
||||
|
||||
@@ -181,10 +181,9 @@ func TestGroupValidate_Failure(t *testing.T) {
|
||||
EvalOffset: promutil.NewDuration(2 * time.Minute),
|
||||
}, false, "eval_offset should be smaller than interval")
|
||||
|
||||
limit := -1
|
||||
f(&Group{
|
||||
Name: "wrong limit",
|
||||
Limit: &limit,
|
||||
Limit: -1,
|
||||
}, false, "invalid limit")
|
||||
|
||||
f(&Group{
|
||||
|
||||
@@ -38,7 +38,7 @@ func (m *manager) groupAPI(gID uint64) (*rule.ApiGroup, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't find group with id %d", gID)
|
||||
}
|
||||
return g.ToAPI(), nil
|
||||
return rule.GroupToAPI(g), nil
|
||||
}
|
||||
|
||||
// ruleAPI generates apiRule object from alert by its ID(hash)
|
||||
@@ -52,7 +52,7 @@ func (m *manager) ruleAPI(gID, rID uint64) (rule.ApiRule, error) {
|
||||
}
|
||||
for _, r := range g.Rules {
|
||||
if r.ID() == rID {
|
||||
return r.ToAPI(), nil
|
||||
return rule.RuleToAPI(r), nil
|
||||
}
|
||||
}
|
||||
return rule.ApiRule{}, fmt.Errorf("can't find rule with id %d in group %q", rID, g.Name)
|
||||
@@ -72,7 +72,7 @@ func (m *manager) alertAPI(gID, aID uint64) (*rule.ApiAlert, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if apiAlert := ar.AlertToAPI(aID); apiAlert != nil {
|
||||
if apiAlert := rule.AlertToAPI(ar, aID); apiAlert != nil {
|
||||
return apiAlert, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,54 +187,6 @@ func (ar *AlertingRule) ID() uint64 {
|
||||
return ar.RuleID
|
||||
}
|
||||
|
||||
// ToAPI returns ApiRule representation of ar
|
||||
func (ar *AlertingRule) ToAPI() ApiRule {
|
||||
state := ar.state
|
||||
lastState := state.getLast()
|
||||
r := ApiRule{
|
||||
Type: TypeAlerting,
|
||||
DatasourceType: ar.Type.String(),
|
||||
Name: ar.Name,
|
||||
Query: ar.Expr,
|
||||
Duration: ar.For.Seconds(),
|
||||
KeepFiringFor: ar.KeepFiringFor.Seconds(),
|
||||
Labels: ar.Labels,
|
||||
Annotations: ar.Annotations,
|
||||
LastEvaluation: lastState.Time,
|
||||
EvaluationTime: lastState.Duration.Seconds(),
|
||||
Health: "ok",
|
||||
State: "inactive",
|
||||
Alerts: ar.AlertsToAPI(),
|
||||
LastSamples: lastState.Samples,
|
||||
LastSeriesFetched: lastState.SeriesFetched,
|
||||
MaxUpdates: state.size(),
|
||||
Updates: state.getAll(),
|
||||
Debug: ar.Debug,
|
||||
|
||||
// encode as strings to avoid rounding in JSON
|
||||
ID: fmt.Sprintf("%d", ar.ID()),
|
||||
GroupID: fmt.Sprintf("%d", ar.GroupID),
|
||||
GroupName: ar.GroupName,
|
||||
File: ar.File,
|
||||
}
|
||||
if lastState.Err != nil {
|
||||
r.LastError = lastState.Err.Error()
|
||||
r.Health = "err"
|
||||
}
|
||||
// satisfy apiRule.State logic
|
||||
if len(r.Alerts) > 0 {
|
||||
r.State = notifier.StatePending.String()
|
||||
stateFiring := notifier.StateFiring.String()
|
||||
for _, a := range r.Alerts {
|
||||
if a.State == stateFiring {
|
||||
r.State = stateFiring
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// GetAlerts returns active alerts of rule
|
||||
func (ar *AlertingRule) GetAlerts() []*notifier.Alert {
|
||||
ar.alertsMu.RLock()
|
||||
|
||||
@@ -24,10 +24,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ruleResultsLimit = flag.Int("rule.resultsLimit", 0, "Limits the number of alerts or recording results a single rule can produce. "+
|
||||
"Can be overridden by the limit option under group if specified. "+
|
||||
"If exceeded, the rule will be marked with an error and all its results will be discarded. "+
|
||||
"0 means no limit.")
|
||||
ruleUpdateEntriesLimit = flag.Int("rule.updateEntriesLimit", 20, "Defines the max number of rule's state updates stored in-memory. "+
|
||||
"Rule's updates are available on rule's Details page and are used for debugging purposes. The number of stored updates can be overridden per rule via update_entries_limit param.")
|
||||
resendDelay = flag.Duration("rule.resendDelay", 0, "MiniMum amount of time to wait before resending an alert to notifier.")
|
||||
@@ -115,6 +111,7 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
|
||||
Name: cfg.Name,
|
||||
File: cfg.File,
|
||||
Interval: cfg.Interval.Duration(),
|
||||
Limit: cfg.Limit,
|
||||
Concurrency: cfg.Concurrency,
|
||||
checksum: cfg.Checksum,
|
||||
Params: cfg.Params,
|
||||
@@ -131,11 +128,6 @@ func NewGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
|
||||
if g.Interval == 0 {
|
||||
g.Interval = defaultInterval
|
||||
}
|
||||
if cfg.Limit != nil {
|
||||
g.Limit = *cfg.Limit
|
||||
} else {
|
||||
g.Limit = *ruleResultsLimit
|
||||
}
|
||||
if g.Concurrency < 1 {
|
||||
g.Concurrency = 1
|
||||
}
|
||||
@@ -296,7 +288,7 @@ func (g *Group) InterruptEval() {
|
||||
}
|
||||
}
|
||||
|
||||
// Close stops the group and its rules, unregisters group metrics
|
||||
// Close stops the group and it's rules, unregisters group metrics
|
||||
func (g *Group) Close() {
|
||||
if g.doneCh == nil {
|
||||
return
|
||||
@@ -305,6 +297,10 @@ func (g *Group) Close() {
|
||||
g.InterruptEval()
|
||||
<-g.finishedCh
|
||||
|
||||
g.closeGroupMetrics()
|
||||
}
|
||||
|
||||
func (g *Group) closeGroupMetrics() {
|
||||
metrics.UnregisterSet(g.metrics.set, true)
|
||||
}
|
||||
|
||||
@@ -334,7 +330,7 @@ func (g *Group) Start(ctx context.Context, nts func() []notifier.Notifier, rw re
|
||||
defer func() { close(g.finishedCh) }()
|
||||
evalTS := time.Now()
|
||||
// sleep random duration to spread group rules evaluation
|
||||
// over time to reduce the load on datasource.
|
||||
// over time in order to reduce load on datasource.
|
||||
if !SkipRandSleepOnGroupStart {
|
||||
sleepBeforeStart := delayBeforeStart(evalTS, g.GetID(), g.Interval, g.EvalOffset)
|
||||
g.infof("will start in %v", sleepBeforeStart)
|
||||
|
||||
@@ -81,37 +81,6 @@ func (rr *RecordingRule) ID() uint64 {
|
||||
return rr.RuleID
|
||||
}
|
||||
|
||||
// ToAPI returns ApiRule representation of rr
|
||||
func (rr *RecordingRule) ToAPI() ApiRule {
|
||||
state := rr.state
|
||||
lastState := state.getLast()
|
||||
r := ApiRule{
|
||||
Type: TypeRecording,
|
||||
DatasourceType: rr.Type.String(),
|
||||
Name: rr.Name,
|
||||
Query: rr.Expr,
|
||||
Labels: rr.Labels,
|
||||
LastEvaluation: lastState.Time,
|
||||
EvaluationTime: lastState.Duration.Seconds(),
|
||||
Health: "ok",
|
||||
LastSamples: lastState.Samples,
|
||||
LastSeriesFetched: lastState.SeriesFetched,
|
||||
MaxUpdates: state.size(),
|
||||
Updates: state.getAll(),
|
||||
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||
GroupName: rr.GroupName,
|
||||
File: rr.File,
|
||||
}
|
||||
if lastState.Err != nil {
|
||||
r.LastError = lastState.Err.Error()
|
||||
r.Health = "err"
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// NewRecordingRule creates a new RecordingRule
|
||||
func NewRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule) *RecordingRule {
|
||||
debug := group.Debug
|
||||
|
||||
@@ -21,8 +21,6 @@ type Rule interface {
|
||||
// ID returns unique ID that may be used for
|
||||
// identifying this Rule among others.
|
||||
ID() uint64
|
||||
// ToAPI returns ApiRule representation of Rule
|
||||
ToAPI() ApiRule
|
||||
// exec executes the rule with given context at the given timestamp and limit.
|
||||
// returns an err if number of resulting time series exceeds the limit.
|
||||
exec(ctx context.Context, ts time.Time, limit int) ([]prompb.TimeSeries, error)
|
||||
@@ -70,6 +68,39 @@ type StateEntry struct {
|
||||
Curl string `json:"curl"`
|
||||
}
|
||||
|
||||
// GetLastEntry returns latest stateEntry of rule
|
||||
func GetLastEntry(r Rule) StateEntry {
|
||||
if rule, ok := r.(*AlertingRule); ok {
|
||||
return rule.state.getLast()
|
||||
}
|
||||
if rule, ok := r.(*RecordingRule); ok {
|
||||
return rule.state.getLast()
|
||||
}
|
||||
return StateEntry{}
|
||||
}
|
||||
|
||||
// GetRuleStateSize returns size of rule stateEntry
|
||||
func GetRuleStateSize(r Rule) int {
|
||||
if rule, ok := r.(*AlertingRule); ok {
|
||||
return rule.state.size()
|
||||
}
|
||||
if rule, ok := r.(*RecordingRule); ok {
|
||||
return rule.state.size()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetAllRuleState returns rule entire stateEntries
|
||||
func GetAllRuleState(r Rule) []StateEntry {
|
||||
if rule, ok := r.(*AlertingRule); ok {
|
||||
return rule.state.getAll()
|
||||
}
|
||||
if rule, ok := r.(*RecordingRule); ok {
|
||||
return rule.state.getAll()
|
||||
}
|
||||
return []StateEntry{}
|
||||
}
|
||||
|
||||
func (s *ruleState) size() int {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
@@ -18,10 +18,8 @@ const (
|
||||
// ParamRuleID is rule id key in url parameter
|
||||
ParamRuleID = "rule_id"
|
||||
|
||||
// TypeRecording is a RecordingRule type
|
||||
TypeRecording = "recording"
|
||||
// TypeAlerting is an AlertingRule type
|
||||
TypeAlerting = "alerting"
|
||||
RuleTypeRecording = "recording"
|
||||
RuleTypeAlerting = "alerting"
|
||||
)
|
||||
|
||||
// ApiGroup represents a Group for web view
|
||||
@@ -197,8 +195,94 @@ func (ar ApiRule) WebLink() string {
|
||||
ParamGroupID, ar.GroupID, ParamRuleID, ar.ID)
|
||||
}
|
||||
|
||||
// AlertsToAPI returns list of ApiAlert objects from existing alerts
|
||||
func (ar *AlertingRule) AlertsToAPI() []*ApiAlert {
|
||||
func RuleToAPI(r any) ApiRule {
|
||||
if ar, ok := r.(*AlertingRule); ok {
|
||||
return alertingToAPI(ar)
|
||||
}
|
||||
if rr, ok := r.(*RecordingRule); ok {
|
||||
return recordingToAPI(rr)
|
||||
}
|
||||
return ApiRule{}
|
||||
}
|
||||
|
||||
func recordingToAPI(rr *RecordingRule) ApiRule {
|
||||
lastState := GetLastEntry(rr)
|
||||
r := ApiRule{
|
||||
Type: RuleTypeRecording,
|
||||
DatasourceType: rr.Type.String(),
|
||||
Name: rr.Name,
|
||||
Query: rr.Expr,
|
||||
Labels: rr.Labels,
|
||||
LastEvaluation: lastState.Time,
|
||||
EvaluationTime: lastState.Duration.Seconds(),
|
||||
Health: "ok",
|
||||
LastSamples: lastState.Samples,
|
||||
LastSeriesFetched: lastState.SeriesFetched,
|
||||
MaxUpdates: GetRuleStateSize(rr),
|
||||
Updates: GetAllRuleState(rr),
|
||||
|
||||
// encode as strings to avoid rounding
|
||||
ID: fmt.Sprintf("%d", rr.ID()),
|
||||
GroupID: fmt.Sprintf("%d", rr.GroupID),
|
||||
GroupName: rr.GroupName,
|
||||
File: rr.File,
|
||||
}
|
||||
if lastState.Err != nil {
|
||||
r.LastError = lastState.Err.Error()
|
||||
r.Health = "err"
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// alertingToAPI returns Rule representation in form of ApiRule
|
||||
func alertingToAPI(ar *AlertingRule) ApiRule {
|
||||
lastState := GetLastEntry(ar)
|
||||
r := ApiRule{
|
||||
Type: RuleTypeAlerting,
|
||||
DatasourceType: ar.Type.String(),
|
||||
Name: ar.Name,
|
||||
Query: ar.Expr,
|
||||
Duration: ar.For.Seconds(),
|
||||
KeepFiringFor: ar.KeepFiringFor.Seconds(),
|
||||
Labels: ar.Labels,
|
||||
Annotations: ar.Annotations,
|
||||
LastEvaluation: lastState.Time,
|
||||
EvaluationTime: lastState.Duration.Seconds(),
|
||||
Health: "ok",
|
||||
State: "inactive",
|
||||
Alerts: RuleToAPIAlert(ar),
|
||||
LastSamples: lastState.Samples,
|
||||
LastSeriesFetched: lastState.SeriesFetched,
|
||||
MaxUpdates: GetRuleStateSize(ar),
|
||||
Updates: GetAllRuleState(ar),
|
||||
Debug: ar.Debug,
|
||||
|
||||
// encode as strings to avoid rounding in JSON
|
||||
ID: fmt.Sprintf("%d", ar.ID()),
|
||||
GroupID: fmt.Sprintf("%d", ar.GroupID),
|
||||
GroupName: ar.GroupName,
|
||||
File: ar.File,
|
||||
}
|
||||
if lastState.Err != nil {
|
||||
r.LastError = lastState.Err.Error()
|
||||
r.Health = "err"
|
||||
}
|
||||
// satisfy apiRule.State logic
|
||||
if len(r.Alerts) > 0 {
|
||||
r.State = notifier.StatePending.String()
|
||||
stateFiring := notifier.StateFiring.String()
|
||||
for _, a := range r.Alerts {
|
||||
if a.State == stateFiring {
|
||||
r.State = stateFiring
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RuleToAPIAlert generates list of apiAlert objects from existing alerts
|
||||
func RuleToAPIAlert(ar *AlertingRule) []*ApiAlert {
|
||||
var alerts []*ApiAlert
|
||||
for _, a := range ar.GetAlerts() {
|
||||
if a.State == notifier.StateInactive {
|
||||
@@ -210,7 +294,7 @@ func (ar *AlertingRule) AlertsToAPI() []*ApiAlert {
|
||||
}
|
||||
|
||||
// AlertToAPI generates apiAlert object from alert by its id(hash)
|
||||
func (ar *AlertingRule) AlertToAPI(id uint64) *ApiAlert {
|
||||
func AlertToAPI(ar *AlertingRule, id uint64) *ApiAlert {
|
||||
a := ar.GetAlert(id)
|
||||
if a == nil {
|
||||
return nil
|
||||
@@ -244,8 +328,7 @@ func NewAlertAPI(ar *AlertingRule, a *notifier.Alert) *ApiAlert {
|
||||
return aa
|
||||
}
|
||||
|
||||
// ToAPI returns ApiGroup representation of g
|
||||
func (g *Group) ToAPI() *ApiGroup {
|
||||
func GroupToAPI(g *Group) *ApiGroup {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
ag := ApiGroup{
|
||||
@@ -270,7 +353,7 @@ func (g *Group) ToAPI() *ApiGroup {
|
||||
}
|
||||
ag.Rules = make([]ApiRule, 0)
|
||||
for _, r := range g.Rules {
|
||||
ag.Rules = append(ag.Rules, r.ToAPI())
|
||||
ag.Rules = append(ag.Rules, RuleToAPI(r))
|
||||
}
|
||||
return &ag
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestRecordingToApi(t *testing.T) {
|
||||
Query: "up",
|
||||
Labels: map[string]string{"label": "value"},
|
||||
Health: "ok",
|
||||
Type: TypeRecording,
|
||||
Type: RuleTypeRecording,
|
||||
DatasourceType: "prometheus",
|
||||
ID: "1248",
|
||||
GroupID: fmt.Sprintf("%d", g.CreateID()),
|
||||
@@ -47,7 +47,7 @@ func TestRecordingToApi(t *testing.T) {
|
||||
Updates: make([]StateEntry, 0),
|
||||
}
|
||||
|
||||
res := rr.ToAPI()
|
||||
res := recordingToAPI(rr)
|
||||
|
||||
if !reflect.DeepEqual(res, expectedRes) {
|
||||
t.Fatalf("expected to have: \n%v;\ngot: \n%v", expectedRes, res)
|
||||
|
||||
@@ -47,8 +47,8 @@ var (
|
||||
{Name: "Docs", URL: "https://docs.victoriametrics.com/victoriametrics/vmalert/"},
|
||||
}
|
||||
ruleTypeMap = map[string]string{
|
||||
"alert": rule.TypeAlerting,
|
||||
"record": rule.TypeRecording,
|
||||
"alert": rule.RuleTypeAlerting,
|
||||
"record": rule.RuleTypeRecording,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -347,7 +347,7 @@ func (rh *requestHandler) groups(rf *rulesFilter) []*rule.ApiGroup {
|
||||
if !rf.matchesGroup(group) {
|
||||
continue
|
||||
}
|
||||
g := group.ToAPI()
|
||||
g := rule.GroupToAPI(group)
|
||||
// the returned list should always be non-nil
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4221
|
||||
filteredRules := make([]rule.ApiRule, 0)
|
||||
@@ -419,11 +419,11 @@ func (rh *requestHandler) groupAlerts() []rule.GroupAlerts {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
alerts = append(alerts, a.AlertsToAPI()...)
|
||||
alerts = append(alerts, rule.RuleToAPIAlert(a)...)
|
||||
}
|
||||
if len(alerts) > 0 {
|
||||
gAlerts = append(gAlerts, rule.GroupAlerts{
|
||||
Group: g.ToAPI(),
|
||||
Group: rule.GroupToAPI(g),
|
||||
Alerts: alerts,
|
||||
})
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func (rh *requestHandler) listAlerts(rf *rulesFilter) ([]byte, error) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, a.AlertsToAPI()...)
|
||||
lr.Data.Alerts = append(lr.Data.Alerts, rule.RuleToAPIAlert(a)...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,13 +85,13 @@ func TestHandler(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("/vmalert/rule", func(t *testing.T) {
|
||||
a := ar.ToAPI()
|
||||
a := rule.RuleToAPI(ar)
|
||||
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
||||
r := rr.ToAPI()
|
||||
r := rule.RuleToAPI(rr)
|
||||
getResp(t, ts.URL+"/vmalert/"+r.WebLink(), nil, 200)
|
||||
})
|
||||
t.Run("/vmalert/alert", func(t *testing.T) {
|
||||
alerts := ar.AlertsToAPI()
|
||||
alerts := rule.RuleToAPIAlert(ar)
|
||||
for _, a := range alerts {
|
||||
getResp(t, ts.URL+"/vmalert/"+a.WebLink(), nil, 200)
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func TestHandler(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("/api/v1/rule?ruleID&groupID", func(t *testing.T) {
|
||||
expRule := ar.ToAPI()
|
||||
expRule := rule.RuleToAPI(ar)
|
||||
gotRule := rule.ApiRule{}
|
||||
getResp(t, ts.URL+"/"+expRule.APILink(), &gotRule, 200)
|
||||
|
||||
@@ -194,7 +194,7 @@ func TestHandler(t *testing.T) {
|
||||
t.Run("/api/v1/group?groupID", func(t *testing.T) {
|
||||
id := groupIDs[0]
|
||||
g := m.groups[id]
|
||||
expGroup := g.ToAPI()
|
||||
expGroup := rule.GroupToAPI(g)
|
||||
gotGroup := rule.ApiGroup{}
|
||||
getResp(t, ts.URL+"/"+expGroup.APILink(), &gotGroup, 200)
|
||||
if expGroup.ID != gotGroup.ID {
|
||||
|
||||
@@ -372,54 +372,20 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
|
||||
updateHeadersByConfig(w.Header(), hc.ResponseHeaders)
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
err = copyStreamToClient(w, res.Body)
|
||||
copyBuf := copyBufPool.Get()
|
||||
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
|
||||
_, err = io.CopyBuffer(w, res.Body, copyBuf.B)
|
||||
copyBufPool.Put(copyBuf)
|
||||
_ = res.Body.Close()
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) && !errors.Is(err, context.Canceled) {
|
||||
if err != nil && !netutil.IsTrivialNetworkError(err) {
|
||||
remoteAddr := httpserver.GetQuotedRemoteAddr(r)
|
||||
requestURI := httpserver.GetRequestURI(r)
|
||||
|
||||
logger.Warnf("remoteAddr: %s; requestURI: %s; error when proxying response body from %s: %s", remoteAddr, requestURI, targetURL, err)
|
||||
return true, false
|
||||
}
|
||||
return true, false
|
||||
}
|
||||
|
||||
func copyStreamToClient(client io.Writer, backend io.Reader) error {
|
||||
copyBuf := copyBufPool.Get()
|
||||
copyBuf.B = bytesutil.ResizeNoCopyNoOverallocate(copyBuf.B, 16*1024)
|
||||
defer copyBufPool.Put(copyBuf)
|
||||
buf := copyBuf.B
|
||||
|
||||
flusher, ok := client.(http.Flusher)
|
||||
if !ok {
|
||||
logger.Panicf("BUG: client must implement net/http.Flusher interface; got %T", client)
|
||||
}
|
||||
|
||||
for {
|
||||
n, backendErr := backend.Read(buf)
|
||||
if n > 0 {
|
||||
data := buf[:n]
|
||||
n, clientErr := client.Write(data)
|
||||
if clientErr != nil {
|
||||
return fmt.Errorf("cannot write data to client: %w", clientErr)
|
||||
}
|
||||
if n != len(data) {
|
||||
logger.Panicf("BUG: unexpected number of bytes written returned by client.Write; got %d; want %d", n, len(data))
|
||||
}
|
||||
// Flush the read data from the backend to the client as fast as possible
|
||||
// in order to reduce delays for data propagation.
|
||||
// See https://github.com/VictoriaMetrics/VictoriaLogs/issues/667
|
||||
flusher.Flush()
|
||||
}
|
||||
if backendErr != nil {
|
||||
if backendErr == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot read data from backend: %w", backendErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var copyBufPool bytesutil.ByteBufferPool
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
|
||||
@@ -514,11 +514,6 @@ func (w *fakeResponseWriter) getResponse() string {
|
||||
return w.bb.String()
|
||||
}
|
||||
|
||||
// Flush implements net/http.Flusher
|
||||
func (w *fakeResponseWriter) Flush() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
func (w *fakeResponseWriter) Header() http.Header {
|
||||
if w.h == nil {
|
||||
w.h = http.Header{}
|
||||
|
||||
@@ -115,7 +115,7 @@ func main() {
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot create backup: %s", err)
|
||||
}
|
||||
pushmetrics.StopAndPush()
|
||||
pushmetrics.Stop()
|
||||
|
||||
startTime := time.Now()
|
||||
logger.Infof("gracefully shutting down http server for metrics at %q", listenAddrs)
|
||||
|
||||
@@ -68,7 +68,7 @@ func main() {
|
||||
if err := a.Run(ctx); err != nil {
|
||||
logger.Fatalf("cannot restore from backup: %s", err)
|
||||
}
|
||||
pushmetrics.StopAndPush()
|
||||
pushmetrics.Stop()
|
||||
srcFS.MustStop()
|
||||
dstFS.MustStop()
|
||||
|
||||
|
||||
@@ -197,13 +197,13 @@ func newNextSeriesForSearchQuery(ec *evalConfig, sq *storage.SearchQuery, expr g
|
||||
}
|
||||
s.summarize(aggrAvg, ec.startTime, ec.endTime, ec.storageStep, 0)
|
||||
t := timerpool.Get(30 * time.Second)
|
||||
defer timerpool.Put(t)
|
||||
select {
|
||||
case seriesCh <- s:
|
||||
case <-t.C:
|
||||
logger.Errorf("resource leak when processing the %s (full query: %s); please report this error to VictoriaMetrics developers",
|
||||
expr.AppendString(nil), ec.originalQuery)
|
||||
}
|
||||
timerpool.Put(t)
|
||||
return nil
|
||||
})
|
||||
close(seriesCh)
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphite"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/prometheus"
|
||||
@@ -20,7 +18,6 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/searchutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/stats"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmstorage"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/flagutil"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
||||
@@ -30,6 +27,7 @@ import (
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/querytracer"
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/timerpool"
|
||||
"github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -742,7 +740,6 @@ var (
|
||||
|
||||
func initVMUIConfig() {
|
||||
var cfg struct {
|
||||
Version string `json:"version"`
|
||||
License struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"license"`
|
||||
@@ -758,11 +755,6 @@ func initVMUIConfig() {
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot parse vmui default config: %s", err)
|
||||
}
|
||||
cfg.Version = buildinfo.ShortVersion()
|
||||
if cfg.Version == "" {
|
||||
// buildinfo.ShortVersion() may return empty result for builds without tags
|
||||
cfg.Version = buildinfo.Version
|
||||
}
|
||||
cfg.VMAlert.Enabled = len(*vmalertProxyURL) != 0
|
||||
data, err = json.Marshal(&cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -1150,23 +1150,15 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||
}
|
||||
qt.Printf("optimized calculation for instant rollup avg_over_time(m[d]) as (sum_over_time(m[d]) / count_over_time(m[d]))")
|
||||
fe := expr.(*metricsql.FuncExpr)
|
||||
// copy RollupExpr to drop possible offset,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
|
||||
newArg.Offset = nil
|
||||
feSum := *fe
|
||||
feSum.Name = "sum_over_time"
|
||||
feCount := *fe
|
||||
feCount.Name = "count_over_time"
|
||||
be := &metricsql.BinaryOpExpr{
|
||||
Op: "/",
|
||||
KeepMetricNames: fe.KeepMetricNames,
|
||||
Left: &metricsql.FuncExpr{
|
||||
Name: "sum_over_time",
|
||||
Args: []metricsql.Expr{newArg},
|
||||
KeepMetricNames: fe.KeepMetricNames,
|
||||
},
|
||||
Right: &metricsql.FuncExpr{
|
||||
Name: "count_over_time",
|
||||
Args: []metricsql.Expr{newArg},
|
||||
KeepMetricNames: fe.KeepMetricNames,
|
||||
},
|
||||
Left: &feSum,
|
||||
Right: &feCount,
|
||||
}
|
||||
return evalExpr(qt, ec, be)
|
||||
case "rate":
|
||||
@@ -1180,12 +1172,8 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||
fe := afe.Args[0].(*metricsql.FuncExpr)
|
||||
feIncrease := *fe
|
||||
feIncrease.Name = "increase"
|
||||
// copy RollupExpr to drop possible offset,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
|
||||
newArg.Offset = nil
|
||||
feIncrease.Args = []metricsql.Expr{newArg}
|
||||
d := newArg.Window.Duration(ec.Step)
|
||||
re := fe.Args[0].(*metricsql.RollupExpr)
|
||||
d := re.Window.Duration(ec.Step)
|
||||
if d == 0 {
|
||||
d = ec.Step
|
||||
}
|
||||
@@ -1205,12 +1193,8 @@ func evalInstantRollup(qt *querytracer.Tracer, ec *EvalConfig, funcName string,
|
||||
fe := expr.(*metricsql.FuncExpr)
|
||||
feIncrease := *fe
|
||||
feIncrease.Name = "increase"
|
||||
// copy RollupExpr to drop possible offset,
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||
newArg := copyRollupExpr(fe.Args[0].(*metricsql.RollupExpr))
|
||||
newArg.Offset = nil
|
||||
feIncrease.Args = []metricsql.Expr{newArg}
|
||||
d := newArg.Window.Duration(ec.Step)
|
||||
re := fe.Args[0].(*metricsql.RollupExpr)
|
||||
d := re.Window.Duration(ec.Step)
|
||||
if d == 0 {
|
||||
d = ec.Step
|
||||
}
|
||||
@@ -2015,23 +1999,3 @@ func dropStaleNaNs(funcName string, values []float64, timestamps []int64) ([]flo
|
||||
}
|
||||
return dstValues, dstTimestamps
|
||||
}
|
||||
|
||||
func copyRollupExpr(re *metricsql.RollupExpr) *metricsql.RollupExpr {
|
||||
var newRe metricsql.RollupExpr
|
||||
newRe.Expr = re.Expr
|
||||
newRe.InheritStep = re.InheritStep
|
||||
newRe.At = re.At
|
||||
if re.Window != nil {
|
||||
newRe.Window = &metricsql.DurationExpr{}
|
||||
*newRe.Window = *re.Window
|
||||
}
|
||||
if re.Offset != nil {
|
||||
newRe.Offset = &metricsql.DurationExpr{}
|
||||
*newRe.Offset = *re.Offset
|
||||
}
|
||||
if re.Step != nil {
|
||||
newRe.Step = &metricsql.DurationExpr{}
|
||||
*newRe.Step = *re.Step
|
||||
}
|
||||
return &newRe
|
||||
}
|
||||
|
||||
1
app/vmselect/vmui/assets/index-Ccv_zSYG.css
Normal file
1
app/vmselect/vmui/assets/index-Ccv_zSYG.css
Normal file
File diff suppressed because one or more lines are too long
209
app/vmselect/vmui/assets/index-DK22yiEQ.js
Normal file
209
app/vmselect/vmui/assets/index-DK22yiEQ.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,10 +36,10 @@
|
||||
<meta property="og:title" content="UI for VictoriaMetrics">
|
||||
<meta property="og:url" content="https://victoriametrics.com/">
|
||||
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
|
||||
<script type="module" crossorigin src="./assets/index-DQcPcJrn.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DY9kCvzk.js">
|
||||
<script type="module" crossorigin src="./assets/index-DK22yiEQ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/vendor-DBOs1yKE.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-dWApsAEM.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Ccv_zSYG.css">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.1 AS build-web-stage
|
||||
FROM golang:1.25.0 AS build-web-stage
|
||||
COPY build /build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<link rel="apple-touch-icon" href="/favicon.svg"/>
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="description" content="Explore and troubleshoot your VictoriaMetrics data"/>
|
||||
|
||||
56
app/vmui/packages/vmui/package-lock.json
generated
56
app/vmui/packages/vmui/package-lock.json
generated
@@ -17,7 +17,7 @@
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^7.6.3",
|
||||
"uplot": "^1.6.32",
|
||||
"vite": "^7.1.5",
|
||||
"vite": "^7.0.4",
|
||||
"web-vitals": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -7321,13 +7321,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -7337,13 +7337,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -7354,9 +7351,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -7660,17 +7657,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
|
||||
"integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz",
|
||||
"integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"fdir": "^6.4.6",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"rollup": "^4.40.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -7775,13 +7772,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
@@ -7792,9 +7786,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^7.6.3",
|
||||
"uplot": "^1.6.32",
|
||||
"vite": "^7.1.5",
|
||||
"vite": "^7.0.4",
|
||||
"web-vitals": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useMemo } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { Alert as APIAlert } from "../../../types";
|
||||
import { createSearchParams } from "react-router-dom";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import Badges from "../Badges";
|
||||
import {
|
||||
SearchIcon,
|
||||
} from "../../Main/Icons";
|
||||
@@ -16,13 +15,6 @@ interface BaseAlertProps {
|
||||
|
||||
const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
const query = item?.expression;
|
||||
const alertLabels = item?.labels || {};
|
||||
const alertLabelsItems = useMemo(() => {
|
||||
return Object.fromEntries(Object.entries(alertLabels).map(([name, value]) => [name, {
|
||||
color: "passive" as BadgeColor,
|
||||
value: value,
|
||||
}]));
|
||||
}, [alertLabels]);
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
@@ -35,10 +27,6 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
return (
|
||||
<div className="vm-explore-alerts-alert-item">
|
||||
<table>
|
||||
<colgroup>
|
||||
<col className="vm-col-md"/>
|
||||
<col/>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
@@ -57,7 +45,7 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Query</td>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<CodeExample
|
||||
code={query}
|
||||
@@ -65,15 +53,18 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Active at</td>
|
||||
<td className="vm-col-md">Active at</td>
|
||||
<td>{dayjs(item.activeAt).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
</tr>
|
||||
{!!Object.keys(alertLabels).length && (
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td>Labels</td>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={alertLabelsItems}
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -84,14 +75,10 @@ const BaseAlert = ({ item }: BaseAlertProps) => {
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col className="vm-col-md"/>
|
||||
<col/>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td>{name}</td>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal.vm-explore-alerts {
|
||||
.vm-modal-content {
|
||||
overflow-y: scroll;
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-alert-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-alert-item {
|
||||
row-gap: $padding-global;
|
||||
margin: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -17,35 +19,36 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-button {
|
||||
color: $color-passive;
|
||||
border: 1px solid $color-passive;
|
||||
}
|
||||
|
||||
.vm-code-example {
|
||||
.vm-button {
|
||||
background-color: $color-code;
|
||||
}
|
||||
border: 1px solid var(--color-passive);
|
||||
}
|
||||
|
||||
table {
|
||||
word-break: break-word;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,17 @@
|
||||
import { useMemo } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { Group as APIGroup } from "../../../types";
|
||||
import dayjs from "dayjs";
|
||||
import { formatDuration } from "../helpers";
|
||||
import Badges, { BadgeColor } from "../Badges";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface BaseGroupProps {
|
||||
group: APIGroup;
|
||||
}
|
||||
|
||||
const BaseGroup = ({ group }: BaseGroupProps) => {
|
||||
const groupLabels = group?.labels || {};
|
||||
const groupLabelsItems = useMemo(() => {
|
||||
return Object.fromEntries(Object.entries(groupLabels).map(([name, value]) => [name, {
|
||||
color: "passive" as BadgeColor,
|
||||
value: value,
|
||||
}]));
|
||||
}, [groupLabels]);
|
||||
|
||||
const groupParams = group?.params || [];
|
||||
const groupParamsItems = useMemo(() => {
|
||||
return Object.fromEntries(groupParams.map(value => [value, {
|
||||
color: "passive" as BadgeColor,
|
||||
}]));
|
||||
}, [groupParams]);
|
||||
|
||||
const groupHeaders = group?.headers || [];
|
||||
const groupHeadersItems = useMemo(() => {
|
||||
return Object.fromEntries(groupHeaders.map(value => [value, {
|
||||
color: "passive" as BadgeColor,
|
||||
}]));
|
||||
}, [groupHeaders]);
|
||||
|
||||
const groupNotifierHeaders = group?.notifier_headers || [];
|
||||
const groupNotifierHeadersItems = useMemo(() => {
|
||||
return Object.fromEntries(groupNotifierHeaders.map(value => [value, {
|
||||
color: "passive" as BadgeColor,
|
||||
}]));
|
||||
}, [groupNotifierHeaders]);
|
||||
return (
|
||||
<div className="vm-explore-alerts-group">
|
||||
<div></div>
|
||||
<table>
|
||||
<tbody>
|
||||
{!!group.interval && (
|
||||
@@ -78,42 +50,51 @@ const BaseGroup = ({ group }: BaseGroupProps) => {
|
||||
<td>{group.concurrency}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!Object.keys(groupLabels).length && (
|
||||
{!!group?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={groupLabelsItems}
|
||||
items={Object.fromEntries(Object.entries(group.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!groupParams.length && (
|
||||
{!!group?.params?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Params</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={groupParamsItems}
|
||||
items={Object.fromEntries(group.params.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!groupHeaders.length && (
|
||||
{!!group?.headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={groupHeadersItems}
|
||||
items={Object.fromEntries(group.headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!groupNotifierHeaders.length && (
|
||||
{!!group?.notifier_headers?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Notifier headers</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={groupNotifierHeadersItems}
|
||||
items={Object.fromEntries(group.notifier_headers.map(value => [value, {
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal.vm-explore-alerts {
|
||||
.vm-modal-content {
|
||||
overflow-y: scroll;
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-group {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-group {
|
||||
row-gap: $padding-global;
|
||||
margin: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -39,13 +41,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
tr.hoverable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
@@ -55,10 +68,6 @@
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useMemo } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import { Rule as APIRule } from "../../../types";
|
||||
import { useNavigate, createSearchParams } from "react-router-dom";
|
||||
@@ -26,14 +25,6 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
};
|
||||
};
|
||||
|
||||
const ruleLabels = item?.labels || {};
|
||||
const ruleLabelsItems = useMemo(() => {
|
||||
return Object.fromEntries(Object.entries(ruleLabels).map(([name, value]) => [name, {
|
||||
color: "passive" as BadgeColor,
|
||||
value: value,
|
||||
}]));
|
||||
}, [ruleLabels]);
|
||||
|
||||
const openQueryLink = () => {
|
||||
const params = {
|
||||
"g0.expr": query,
|
||||
@@ -44,11 +35,8 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
|
||||
return (
|
||||
<div className="vm-explore-alerts-rule-item">
|
||||
<div></div>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col className="vm-col-md"/>
|
||||
<col/>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
@@ -67,7 +55,7 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Query</td>
|
||||
<td className="vm-col-md">Query</td>
|
||||
<td>
|
||||
<CodeExample
|
||||
code={query}
|
||||
@@ -76,30 +64,33 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
</tr>
|
||||
{!!item.duration && (
|
||||
<tr>
|
||||
<td>For</td>
|
||||
<td className="vm-col-md">For</td>
|
||||
<td>{formatDuration(item.duration)}</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!item.lastEvaluation && (
|
||||
<tr>
|
||||
<td>Last evaluation</td>
|
||||
<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>Last error</td>
|
||||
<td className="vm-col-md">Last error</td>
|
||||
<td>
|
||||
<Alert variant="error">{item.lastError}</Alert>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!!Object.keys(ruleLabelsItems).length && (
|
||||
{!!Object.keys(item?.labels || {}).length && (
|
||||
<tr>
|
||||
<td>Labels</td>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={ruleLabelsItems}
|
||||
items={Object.fromEntries(Object.entries(item.labels).map(([name, value]) => [name, {
|
||||
color: "passive",
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -109,15 +100,11 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
{!!Object.keys(item?.annotations || {}).length && (
|
||||
<>
|
||||
<span className="title">Annotations</span>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col className="vm-col-md"/>
|
||||
<col/>
|
||||
</colgroup>
|
||||
<table className="fixed">
|
||||
<tbody>
|
||||
{Object.entries(item.annotations || {}).map(([name, value]) => (
|
||||
<tr key={name}>
|
||||
<td>{name}</td>
|
||||
<td className="vm-col-md">{name}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -128,14 +115,14 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
{!!item?.updates?.length && (
|
||||
<>
|
||||
<span className="title">{`Last updates ${item.updates.length}/${item.max_updates_entries}`}</span>
|
||||
<table>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Updated at</th>
|
||||
<th>Series returned</th>
|
||||
<th>Series fetched</th>
|
||||
<th>Duration</th>
|
||||
<th>Executed at</th>
|
||||
<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>
|
||||
@@ -143,11 +130,11 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
<tr
|
||||
key={update.at}
|
||||
>
|
||||
<td>{dayjs(update.time).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
<td>{update.samples}</td>
|
||||
<td>{update.series_fetched}</td>
|
||||
<td>{formatDuration(update.duration / 1e9)}</td>
|
||||
<td>{dayjs(update.at).format("DD MMM YYYY HH:mm:ss")}</td>
|
||||
<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>
|
||||
@@ -157,21 +144,14 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
{!!item?.alerts?.length && (
|
||||
<>
|
||||
<span className="title">Alerts</span>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col className="vm-col-sm"/>
|
||||
<col className="vm-col-sm"/>
|
||||
<col className="vm-col-sm"/>
|
||||
<col/>
|
||||
<col className="vm-col-hidden"/>
|
||||
</colgroup>
|
||||
<table className="fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Active since</th>
|
||||
<th>State</th>
|
||||
<th>Value</th>
|
||||
<th className="title">Labels</th>
|
||||
<th></th>
|
||||
<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>
|
||||
@@ -180,15 +160,15 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
id={`alert-${alert.id}`}
|
||||
key={alert.id}
|
||||
>
|
||||
<td>
|
||||
<td className="vm-col-sm">
|
||||
{dayjs(alert.activeAt).format("DD MMM YYYY HH:mm:ss")}
|
||||
</td>
|
||||
<td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.state]: { color: alert.state as BadgeColor } }}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td className="vm-col-sm">
|
||||
<Badges
|
||||
items={{ [alert.value]: { color: "passive" } }}
|
||||
/>
|
||||
@@ -202,7 +182,7 @@ const BaseRule = ({ item }: BaseRuleProps) => {
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td className="vm-col-hidden">
|
||||
<Button
|
||||
className="vm-button-borderless"
|
||||
size="small"
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-modal.vm-explore-alerts {
|
||||
.vm-modal-content {
|
||||
overflow-y: scroll;
|
||||
.vm-modal {
|
||||
.vm-explore-alerts-rule-item {
|
||||
table {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-rule-item {
|
||||
row-gap: $padding-global;
|
||||
margin: $padding-global;
|
||||
margin-right: $padding-global;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -18,46 +20,46 @@
|
||||
}
|
||||
|
||||
.vm-col-hidden {
|
||||
width: 40px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.vm-button {
|
||||
color: $color-passive;
|
||||
border: 1px solid $color-passive;
|
||||
}
|
||||
|
||||
.vm-code-example {
|
||||
.vm-button {
|
||||
background-color: $color-code;
|
||||
}
|
||||
border: 1px solid var(--color-passive);
|
||||
}
|
||||
|
||||
.vm-col-sm {
|
||||
width: 15%;
|
||||
width: 10%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.vm-col-md {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
word-break: break-word;
|
||||
table-layout: fixed;
|
||||
&.fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
width: 100%;
|
||||
td, th {
|
||||
line-height: 30px;
|
||||
padding: 4px $padding-small;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
td.align-center {
|
||||
text-align: center
|
||||
}
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0 $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +35,12 @@ const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
|
||||
<div className="vm-explore-alerts-group-header__file">{group.file}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-explore-alerts-controls">
|
||||
<Badges
|
||||
align="end"
|
||||
items={Object.fromEntries(Object.entries(group.states || {}).map(([name, value]) => [name.toLowerCase(), {
|
||||
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||
value: value,
|
||||
}]))}
|
||||
/>
|
||||
<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"
|
||||
@@ -51,7 +49,7 @@ const GroupHeaderHeader: FC<GroupHeaderControlsProps> = ({ group }) => {
|
||||
startIcon={<DetailsIcon />}
|
||||
onClick={openGroupModal}
|
||||
/>
|
||||
</div>
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,8 +58,3 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-controls {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
import { FC } from "preact/compat";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useCopyToClipboard from "../../../hooks/useCopyToClipboard";
|
||||
@@ -83,13 +83,6 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
|
||||
}
|
||||
};
|
||||
|
||||
const badgesItems = useMemo(() => {
|
||||
return Object.fromEntries(Object.entries(states || {}).map(([name, value]) => [name, {
|
||||
color: name.toLowerCase().replace(" ", "-") as BadgeColor,
|
||||
value: value == 1 ? 0 : value,
|
||||
}]));
|
||||
}, [states]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={headerClasses}
|
||||
@@ -99,11 +92,12 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
|
||||
{renderIcon()}
|
||||
<div className="vm-explore-alerts-item-header__name">{name}</div>
|
||||
</div>
|
||||
<div className="vm-explore-alerts-controls">
|
||||
<Badges
|
||||
align="end"
|
||||
items={badgesItems}
|
||||
/>
|
||||
<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"
|
||||
@@ -125,7 +119,7 @@ const ItemHeader: FC<ItemHeaderControlsProps> = ({ name, id, groupId, entity, ty
|
||||
onClick={openItemLink}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Badges>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.vm-explore-alerts-item-header {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
@@ -27,6 +28,7 @@
|
||||
}
|
||||
|
||||
&_mobile {
|
||||
grid-template-columns: 1fr auto;
|
||||
.vm-button-text {
|
||||
display: none;
|
||||
}
|
||||
@@ -49,11 +51,9 @@
|
||||
&__title {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
overflow: hidden;
|
||||
svg {
|
||||
fill: $color-text-disabled;
|
||||
width: 14px;
|
||||
min-width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,3 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.vm-explore-alerts-controls {
|
||||
display: flex;
|
||||
column-gap: $padding-global;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,26 @@
|
||||
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 {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC, useMemo } from "preact/compat";
|
||||
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, { BadgeColor } from "../Badges";
|
||||
import Badges from "../Badges";
|
||||
|
||||
interface TargetProps {
|
||||
target: APITarget;
|
||||
@@ -11,13 +11,6 @@ interface TargetProps {
|
||||
|
||||
const Target: FC<TargetProps> = ({ target }) => {
|
||||
const state = target?.lastError ? "unhealthy" : "ok";
|
||||
const targetLabels = target?.labels || {};
|
||||
const badgesItems = useMemo(() => {
|
||||
return Object.fromEntries(Object.entries(targetLabels).map(([name, value]) => [name, {
|
||||
value: value,
|
||||
color: "passive" as BadgeColor,
|
||||
}]));
|
||||
}, [targetLabels]);
|
||||
return (
|
||||
<div className={`vm-explore-alerts-target vm-badge-item ${state.replace(" ", "-")}`}>
|
||||
{(!!target?.labels?.length || !!target?.lastError) ? (
|
||||
@@ -30,12 +23,15 @@ const Target: FC<TargetProps> = ({ target }) => {
|
||||
<div className="vm-explore-alerts-target-item">
|
||||
<table>
|
||||
<tbody>
|
||||
{!!Object.keys(targetLabels).length && (
|
||||
{!!target?.labels?.length && (
|
||||
<tr>
|
||||
<td className="vm-col-md">Labels</td>
|
||||
<td>
|
||||
<Badges
|
||||
items={badgesItems}
|
||||
items={Object.fromEntries(Object.entries(target.labels).map(([name, value]) => [name, {
|
||||
value: value,
|
||||
color: "passive",
|
||||
}]))}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
padding: $padding-global;
|
||||
white-space: pre-wrap;
|
||||
border-radius: $border-radius-small;
|
||||
background-color: $color-code;
|
||||
background-color: rgba($color-black, 0.05);
|
||||
overflow: auto;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&__copy {
|
||||
position: absolute;
|
||||
|
||||
@@ -13,8 +13,6 @@ import Button from "../../Button/Button";
|
||||
interface DatePickerProps {
|
||||
date: Date | Dayjs
|
||||
format?: string
|
||||
minDate?: Date | Dayjs
|
||||
maxDate?: Date | Dayjs
|
||||
onChange: (date: string) => void
|
||||
}
|
||||
|
||||
@@ -26,8 +24,6 @@ enum CalendarTypeView {
|
||||
|
||||
const Calendar: FC<DatePickerProps> = ({
|
||||
date,
|
||||
minDate,
|
||||
maxDate,
|
||||
format = DATE_TIME_FORMAT,
|
||||
onChange,
|
||||
}) => {
|
||||
@@ -38,8 +34,6 @@ const Calendar: FC<DatePickerProps> = ({
|
||||
const today = dayjs.tz();
|
||||
const viewDateIsToday = today.format(DATE_FORMAT) === viewDate.format(DATE_FORMAT);
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const min = minDate ? dayjs(minDate) : undefined;
|
||||
const max = maxDate ? dayjs(maxDate) : undefined;
|
||||
|
||||
const toggleDisplayYears = () => {
|
||||
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.days : CalendarTypeView.years);
|
||||
@@ -81,13 +75,9 @@ const Calendar: FC<DatePickerProps> = ({
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
toggleDisplayYears={toggleDisplayYears}
|
||||
showArrowNav={viewType === CalendarTypeView.days}
|
||||
hasPrev={viewType === CalendarTypeView.days && (!min || viewDate.startOf("month").isAfter(min))}
|
||||
hasNext={viewType === CalendarTypeView.days && (!max || viewDate.endOf("month").isBefore(max))}
|
||||
/>
|
||||
{viewType === CalendarTypeView.days && (
|
||||
<CalendarBody
|
||||
minDate={min}
|
||||
maxDate={max}
|
||||
viewDate={viewDate}
|
||||
selectDate={selectDate}
|
||||
onChangeSelectDate={handleChangeSelectDate}
|
||||
@@ -95,16 +85,12 @@ const Calendar: FC<DatePickerProps> = ({
|
||||
)}
|
||||
{viewType === CalendarTypeView.years && (
|
||||
<YearsList
|
||||
minDate={min}
|
||||
maxDate={max}
|
||||
viewDate={viewDate}
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
/>
|
||||
)}
|
||||
{viewType === CalendarTypeView.months && (
|
||||
<MonthsList
|
||||
minDate={min}
|
||||
maxDate={max}
|
||||
selectDate={selectDate}
|
||||
viewDate={viewDate}
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
|
||||
@@ -4,8 +4,6 @@ import classNames from "classnames";
|
||||
import Tooltip from "../../../Tooltip/Tooltip";
|
||||
|
||||
interface CalendarBodyProps {
|
||||
minDate?: Dayjs
|
||||
maxDate?: Dayjs
|
||||
viewDate: Dayjs
|
||||
selectDate: Dayjs
|
||||
onChangeSelectDate: (date: Dayjs) => void
|
||||
@@ -13,7 +11,7 @@ interface CalendarBodyProps {
|
||||
|
||||
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
const CalendarBody: FC<CalendarBodyProps> = ({ minDate, maxDate, viewDate: date, selectDate, onChangeSelectDate }) => {
|
||||
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate: date, selectDate, onChangeSelectDate }) => {
|
||||
const format = "YYYY-MM-DD";
|
||||
const today = dayjs.tz();
|
||||
const viewDate = dayjs(date.format(format));
|
||||
@@ -46,25 +44,21 @@ const CalendarBody: FC<CalendarBodyProps> = ({ minDate, maxDate, viewDate: date,
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{days.map((d, i) => {
|
||||
const isDisabled = d && ((minDate && d.isBefore(minDate)) || (maxDate && d.isAfter(maxDate)));
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-body-cell": true,
|
||||
"vm-calendar-body-cell_day": true,
|
||||
"vm-calendar-body-cell_day_empty": !d,
|
||||
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
|
||||
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format),
|
||||
"vm-calendar-body-cell_day_disabled": isDisabled,
|
||||
})}
|
||||
key={d ? d.format(format) : i}
|
||||
onClick={createHandlerSelectDate(d)}
|
||||
>
|
||||
{d && d.format("D")}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{days.map((d, i) => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-body-cell": true,
|
||||
"vm-calendar-body-cell_day": true,
|
||||
"vm-calendar-body-cell_day_empty": !d,
|
||||
"vm-calendar-body-cell_day_active": (d && d.format(format)) === selectDate.format(format),
|
||||
"vm-calendar-body-cell_day_today": (d && d.format(format)) === today.format(format)
|
||||
})}
|
||||
key={d ? d.format(format) : i}
|
||||
onClick={createHandlerSelectDate(d)}
|
||||
>
|
||||
{d && d.format("D")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { FC } from "preact/compat";
|
||||
import { Dayjs } from "dayjs";
|
||||
import { ArrowDownIcon, ArrowDropDownIcon } from "../../../Icons";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface CalendarHeaderProps {
|
||||
viewDate: Dayjs
|
||||
onChangeViewDate: (date: Dayjs) => void
|
||||
showArrowNav: boolean
|
||||
toggleDisplayYears: () => void
|
||||
hasNext: boolean
|
||||
hasPrev: boolean
|
||||
}
|
||||
|
||||
const CalendarHeader: FC<CalendarHeaderProps> = ({ hasPrev, hasNext, viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
|
||||
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
|
||||
|
||||
const setPrevMonth = () => {
|
||||
onChangeViewDate(viewDate.subtract(1, "month"));
|
||||
@@ -38,20 +35,14 @@ const CalendarHeader: FC<CalendarHeaderProps> = ({ hasPrev, hasNext, viewDate, s
|
||||
{showArrowNav && (
|
||||
<div className="vm-calendar-header-right">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-header-right__prev": true,
|
||||
"vm-calendar-header-right_disabled": !hasPrev,
|
||||
})}
|
||||
onClick={hasPrev ? setPrevMonth : undefined}
|
||||
className="vm-calendar-header-right__prev"
|
||||
onClick={setPrevMonth}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-header-right__next": true,
|
||||
"vm-calendar-header-right_disabled": !hasNext,
|
||||
})}
|
||||
onClick={hasNext ? setNextMonth : undefined}
|
||||
className="vm-calendar-header-right__next"
|
||||
onClick={setNextMonth}
|
||||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,13 @@ import dayjs, { Dayjs } from "dayjs";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface CalendarMonthsProps {
|
||||
minDate?: Dayjs
|
||||
maxDate?: Dayjs
|
||||
viewDate: Dayjs,
|
||||
selectDate: Dayjs
|
||||
|
||||
onChangeViewDate: (date: Dayjs) => void
|
||||
}
|
||||
|
||||
const MonthsList: FC<CalendarMonthsProps> = ({ minDate, maxDate, viewDate, selectDate, onChangeViewDate }) => {
|
||||
const MonthsList: FC<CalendarMonthsProps> = ({ viewDate, selectDate, onChangeViewDate }) => {
|
||||
|
||||
const today = dayjs().format("MM");
|
||||
const currentMonths = useMemo(() => selectDate.format("MM"), [selectDate]);
|
||||
@@ -30,24 +29,20 @@ const MonthsList: FC<CalendarMonthsProps> = ({ minDate, maxDate, viewDate, selec
|
||||
|
||||
return (
|
||||
<div className="vm-calendar-years">
|
||||
{months.map(m => {
|
||||
const isDisabled = m && ((minDate && m.isBefore(minDate)) || (maxDate && m.isAfter(maxDate)));
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-years__year": true,
|
||||
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
|
||||
"vm-calendar-years__year_today": m.format("MM") === today,
|
||||
"vm-calendar-years__year_disabled": isDisabled,
|
||||
})}
|
||||
id={`vm-calendar-year-${m.format("MM")}`}
|
||||
key={m.format("MM")}
|
||||
onClick={isDisabled ? undefined : createHandlerClick(m)}
|
||||
>
|
||||
{m.format("MMMM")}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{months.map(m => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-years__year": true,
|
||||
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
|
||||
"vm-calendar-years__year_today": m.format("MM") === today
|
||||
})}
|
||||
id={`vm-calendar-year-${m.format("MM")}`}
|
||||
key={m.format("MM")}
|
||||
onClick={createHandlerClick(m)}
|
||||
>
|
||||
{m.format("MMMM")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,13 +3,11 @@ import dayjs, { Dayjs } from "dayjs";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface CalendarYearsProps {
|
||||
minDate?: Dayjs
|
||||
maxDate?: Dayjs
|
||||
viewDate: Dayjs
|
||||
onChangeViewDate: (date: Dayjs) => void
|
||||
}
|
||||
|
||||
const YearsList: FC<CalendarYearsProps> = ({ minDate, maxDate, viewDate, onChangeViewDate }) => {
|
||||
const YearsList: FC<CalendarYearsProps> = ({ viewDate, onChangeViewDate }) => {
|
||||
|
||||
const today = dayjs().format("YYYY");
|
||||
const currentYear = useMemo(() => viewDate.format("YYYY"), [viewDate]);
|
||||
@@ -32,24 +30,20 @@ const YearsList: FC<CalendarYearsProps> = ({ minDate, maxDate, viewDate, onChang
|
||||
|
||||
return (
|
||||
<div className="vm-calendar-years">
|
||||
{years.map(y => {
|
||||
const isDisabled = y && (minDate && y.isBefore(minDate)) || (maxDate && y.isAfter(maxDate));
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-years__year": true,
|
||||
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
|
||||
"vm-calendar-years__year_today": y.format("YYYY") === today,
|
||||
"vm-calendar-years__year_disabled": isDisabled,
|
||||
})}
|
||||
id={`vm-calendar-year-${y.format("YYYY")}`}
|
||||
key={y.format("YYYY")}
|
||||
onClick={isDisabled ? undefined : createHandlerClick(y)}
|
||||
>
|
||||
{y.format("YYYY")}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{years.map(y => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-years__year": true,
|
||||
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
|
||||
"vm-calendar-years__year_today": y.format("YYYY") === today
|
||||
})}
|
||||
id={`vm-calendar-year-${y.format("YYYY")}`}
|
||||
key={y.format("YYYY")}
|
||||
onClick={createHandlerClick(y)}
|
||||
>
|
||||
{y.format("YYYY")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,10 +69,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&_disabled {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&__prev {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@@ -112,12 +108,7 @@
|
||||
cursor: pointer;
|
||||
transition: color 200ms ease, background-color 300ms ease-in-out;
|
||||
|
||||
&_disabled {
|
||||
cursor: unset;
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&:not(&_disabled):hover {
|
||||
&:hover {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
@@ -157,12 +148,7 @@
|
||||
cursor: pointer;
|
||||
transition: color 200ms ease, background-color 300ms ease-in-out;
|
||||
|
||||
&_disabled {
|
||||
cursor: unset;
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&:not(&_disabled):hover {
|
||||
&:hover {
|
||||
background-color: $color-hover-black;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,8 @@ import useEventListener from "../../../hooks/useEventListener";
|
||||
interface DatePickerProps {
|
||||
date: string | Date | Dayjs,
|
||||
targetRef: React.RefObject<HTMLElement>;
|
||||
format?: string;
|
||||
label?: string;
|
||||
minDate?: Date | Dayjs;
|
||||
maxDate?: Date | Dayjs;
|
||||
format?: string
|
||||
label?: string
|
||||
onChange: (val: string) => void
|
||||
}
|
||||
|
||||
@@ -22,9 +20,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||
targetRef,
|
||||
format = DATE_TIME_FORMAT,
|
||||
onChange,
|
||||
label,
|
||||
minDate,
|
||||
maxDate
|
||||
label
|
||||
}, ref) => {
|
||||
const dateDayjs = useMemo(() => dayjs(date).isValid() ? dayjs.tz(date) : dayjs().tz(), [date]);
|
||||
const { isMobile } = useDeviceDetect();
|
||||
@@ -60,8 +56,6 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||
date={dateDayjs}
|
||||
format={format}
|
||||
onChange={handleChangeDate}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
/>
|
||||
</div>
|
||||
</Popper>
|
||||
|
||||
@@ -3,52 +3,37 @@ import { ChangeEvent, KeyboardEvent } from "react";
|
||||
import { CalendarIcon } from "../../Icons";
|
||||
import DatePicker from "../DatePicker";
|
||||
import Button from "../../Button/Button";
|
||||
import { DATE_ISO_FORMAT, DATE_FORMAT, DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||
import InputMask from "react-input-mask";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
|
||||
const formatStringDate = (val: string, format: string) => {
|
||||
return dayjs(val).isValid() ? dayjs.tz(val).format(format) : val;
|
||||
const formatStringDate = (val: string) => {
|
||||
return dayjs(val).isValid() ? dayjs.tz(val).format(DATE_TIME_FORMAT) : val;
|
||||
};
|
||||
|
||||
interface DateTimeInputProps {
|
||||
value?: string;
|
||||
label: string;
|
||||
pickerLabel: string;
|
||||
format?: string;
|
||||
pickerRef: React.RefObject<HTMLDivElement>;
|
||||
onChange: (date: string) => void;
|
||||
onEnter: () => void;
|
||||
disabled?: boolean;
|
||||
minDate?: Date | Dayjs;
|
||||
maxDate?: Date | Dayjs;
|
||||
}
|
||||
|
||||
const masks: Record<string, string> = {
|
||||
[DATE_ISO_FORMAT]: "9999-99-99T99:99:99",
|
||||
[DATE_FORMAT]: "9999-99-99",
|
||||
[DATE_TIME_FORMAT]: "9999-99-99 99:99:99"
|
||||
};
|
||||
|
||||
const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
value = "",
|
||||
format = DATE_TIME_FORMAT,
|
||||
minDate,
|
||||
maxDate,
|
||||
label,
|
||||
pickerLabel,
|
||||
pickerRef,
|
||||
onChange,
|
||||
onEnter,
|
||||
disabled
|
||||
onEnter
|
||||
}) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
|
||||
const mask = masks[format];
|
||||
|
||||
const [maskedValue, setMaskedValue] = useState(formatStringDate(value, format));
|
||||
const [maskedValue, setMaskedValue] = useState(formatStringDate(value));
|
||||
const [focusToTime, setFocusToTime] = useState(false);
|
||||
const [awaitChangeForEnter, setAwaitChangeForEnter] = useState(false);
|
||||
const error = dayjs(maskedValue).isValid() ? "" : "Invalid date format";
|
||||
@@ -74,7 +59,7 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const newValue = formatStringDate(value, format);
|
||||
const newValue = formatStringDate(value);
|
||||
if (newValue !== maskedValue) {
|
||||
setMaskedValue(newValue);
|
||||
}
|
||||
@@ -97,16 +82,15 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-date-time-input": true,
|
||||
"vm-date-time-input_error": error,
|
||||
"vm-date-time-input_disabled": disabled,
|
||||
"vm-date-time-input_error": error
|
||||
})}
|
||||
>
|
||||
<label>{label}</label>
|
||||
<InputMask
|
||||
tabIndex={1}
|
||||
inputRef={setInputRef}
|
||||
mask={mask}
|
||||
placeholder={format}
|
||||
mask="9999-99-99 99:99:99"
|
||||
placeholder="YYYY-MM-DD HH:mm:ss"
|
||||
value={maskedValue}
|
||||
autoCapitalize={"none"}
|
||||
inputMode={"numeric"}
|
||||
@@ -114,7 +98,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
onChange={handleMaskedChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{error && (
|
||||
<span className="vm-date-time-input__error-text">{error}</span>
|
||||
@@ -129,7 +112,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
size="small"
|
||||
startIcon={<CalendarIcon/>}
|
||||
ariaLabel="calendar"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<DatePicker
|
||||
@@ -138,9 +120,6 @@ const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
date={maskedValue}
|
||||
onChange={handleChangeDate}
|
||||
targetRef={wrapperRef}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
format={format}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,14 +23,6 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&_disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
* {
|
||||
color: $color-text-disabled !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
|
||||
@@ -22,8 +22,8 @@ interface PopperProps {
|
||||
children: ReactNode
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
buttonRef?: React.RefObject<HTMLElement>
|
||||
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right" | "center-left" | "center-right" | "fixed"
|
||||
buttonRef: React.RefObject<HTMLElement>
|
||||
placement?: "bottom-right" | "bottom-left" | "top-left" | "top-right" | "fixed"
|
||||
placementPosition?: { top: number, left: number } | null
|
||||
animation?: string
|
||||
offset?: { top: number, left: number }
|
||||
@@ -32,7 +32,6 @@ interface PopperProps {
|
||||
title?: string
|
||||
disabledFullScreen?: boolean
|
||||
variant?: "default" | "dark"
|
||||
classes?: string[]
|
||||
}
|
||||
|
||||
const Popper: FC<PopperProps> = ({
|
||||
@@ -47,7 +46,6 @@ const Popper: FC<PopperProps> = ({
|
||||
fullWidth,
|
||||
title,
|
||||
disabledFullScreen,
|
||||
classes,
|
||||
variant
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
@@ -80,7 +78,6 @@ const Popper: FC<PopperProps> = ({
|
||||
}, [popperRef]);
|
||||
|
||||
const popperStyle = useMemo(() => {
|
||||
if (!buttonRef) return {};
|
||||
const buttonEl = buttonRef.current;
|
||||
|
||||
if (!buttonEl || !isOpen) return {};
|
||||
@@ -93,9 +90,8 @@ const Popper: FC<PopperProps> = ({
|
||||
width: "auto"
|
||||
};
|
||||
|
||||
const needAlignRight = placement?.includes("right");
|
||||
const needAlignRight = placement === "bottom-right" || placement === "top-right";
|
||||
const needAlignTop = placement?.includes("top");
|
||||
const needAlignCenter = placement?.includes("center");
|
||||
|
||||
const offsetTop = offset?.top || 0;
|
||||
const offsetLeft = offset?.left || 0;
|
||||
@@ -105,7 +101,6 @@ const Popper: FC<PopperProps> = ({
|
||||
|
||||
if (needAlignRight) position.left = buttonPos.right - popperSize.width;
|
||||
if (needAlignTop) position.top = buttonPos.top - popperSize.height - offsetTop;
|
||||
if (needAlignCenter) position.top = buttonPos.top + (buttonPos.height - popperSize.height) / 2 - offsetTop;
|
||||
|
||||
if (placement === "fixed" && placementPosition) {
|
||||
position.top = Math.max(placementPosition.top + offset.top, 0);
|
||||
@@ -166,10 +161,6 @@ const Popper: FC<PopperProps> = ({
|
||||
useEventListener("scroll", handleClose);
|
||||
useEventListener("popstate", handlePopstate);
|
||||
useClickOutside(popperRef, handleClickOutside, buttonRef);
|
||||
const classMap: Record<string, boolean> = {};
|
||||
(classes || []).forEach((cls) => {
|
||||
classMap[cls] = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -180,7 +171,6 @@ const Popper: FC<PopperProps> = ({
|
||||
[`vm-popper_${variant}`]: variant,
|
||||
"vm-popper_mobile": isMobile && !disabledFullScreen,
|
||||
"vm-popper_open": (isMobile || Object.keys(popperStyle).length) && isOpen,
|
||||
...classMap,
|
||||
})}
|
||||
ref={popperRef}
|
||||
style={(isMobile && !disabledFullScreen) ? {} : popperStyle}
|
||||
|
||||
@@ -46,12 +46,11 @@ const Select: FC<SelectProps> = ({
|
||||
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
|
||||
const [wrapperRef, setWrapperRef] = useState<React.RefObject<HTMLElement> | null>(null);
|
||||
const [openList, setOpenList] = useState(false);
|
||||
const resultList = [...list];
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMultiple = Array.isArray(value);
|
||||
let selectedValues = Array.isArray(value) ? value.slice() : [];
|
||||
const selectedValues = Array.isArray(value) ? value.slice() : [];
|
||||
const hideInput = isMobile && isMultiple && !!selectedValues?.length;
|
||||
|
||||
const textFieldValue = useMemo(() => {
|
||||
@@ -78,7 +77,7 @@ const Select: FC<SelectProps> = ({
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
resultList.includes(search) && onChange(search);
|
||||
list.includes(search) && onChange(search);
|
||||
};
|
||||
|
||||
const handleToggleList = (e: MouseEvent<HTMLDivElement>) => {
|
||||
@@ -124,10 +123,8 @@ const Select: FC<SelectProps> = ({
|
||||
useEventListener("keyup", handleKeyUp);
|
||||
useClickOutside(autocompleteAnchorEl, handleCloseList, wrapperRef);
|
||||
|
||||
if (includeAll && !resultList.includes("All")) resultList.push("All");
|
||||
if (includeAll && (!selectedValues?.length || selectedValues?.length === resultList?.length)) {
|
||||
selectedValues = ["All"];
|
||||
}
|
||||
includeAll && !list.includes("All") && list.push("All");
|
||||
includeAll && !selectedValues?.length && selectedValues.push("All");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -158,7 +155,6 @@ const Select: FC<SelectProps> = ({
|
||||
onInput={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
ref={inputRef}
|
||||
readOnly={isMobile || !searchable}
|
||||
/>
|
||||
@@ -186,7 +182,7 @@ const Select: FC<SelectProps> = ({
|
||||
itemClassName={itemClassName}
|
||||
label={label}
|
||||
value={autocompleteValue}
|
||||
options={resultList.map(l => ({ value: l }))}
|
||||
options={list.map(l => ({ value: l }))}
|
||||
anchor={autocompleteAnchorEl}
|
||||
selected={selectedValues}
|
||||
minLength={1}
|
||||
|
||||
@@ -128,14 +128,8 @@
|
||||
}
|
||||
|
||||
&_disabled {
|
||||
pointer-events: none;
|
||||
* {
|
||||
color: var(--color-text-disabled);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--color-text-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.vm-select-input {
|
||||
|
||||
@@ -9,7 +9,6 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
|
||||
.vm-switch {
|
||||
font-size: $font-size-small;
|
||||
display: flex;
|
||||
column-gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
@@ -20,6 +19,10 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&_full-width &__label {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&_disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
@@ -71,6 +74,7 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
|
||||
&__label {
|
||||
white-space: nowrap;
|
||||
font-size: inherit;
|
||||
margin-left: $padding-small;
|
||||
transition: color 200ms ease;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
"vm-text-field__input_error": error,
|
||||
"vm-text-field__input_warning": !error && warning,
|
||||
"vm-text-field__input_icon-start": startIcon,
|
||||
"vm-text-field__input_disabled": disabled,
|
||||
"vm-text-field__input_textarea": type === "textarea",
|
||||
});
|
||||
|
||||
@@ -135,8 +136,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
className={classNames({
|
||||
"vm-text-field": true,
|
||||
"vm-text-field_textarea": type === "textarea",
|
||||
"vm-text-field_dark": isDarkTheme,
|
||||
"vm-text-field_disabled": disabled
|
||||
"vm-text-field_dark": isDarkTheme
|
||||
})}
|
||||
data-replicated-value={value}
|
||||
>
|
||||
|
||||
@@ -6,15 +6,6 @@
|
||||
margin: 6px 0;
|
||||
width: 100%;
|
||||
|
||||
&_disabled {
|
||||
color: $color-text-disabled;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:is(&_disabled) > &__label {
|
||||
color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&_textarea:after {
|
||||
content: attr(data-replicated-value) " ";
|
||||
white-space: pre-wrap;
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
import { createRef, useEffect, useState, useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import classNames from "classnames";
|
||||
import { ArrowDropDownIcon, CopyIcon, DoneIcon } from "../Main/Icons";
|
||||
import { getComparator, stableSort } from "./helpers";
|
||||
import Tooltip from "../Main/Tooltip/Tooltip";
|
||||
import Button from "../Main/Button/Button";
|
||||
import { useEffect } from "preact/compat";
|
||||
import useCopyToClipboard from "../../hooks/useCopyToClipboard";
|
||||
|
||||
type OrderDir = "asc" | "desc"
|
||||
|
||||
export interface TableColumn<T> {
|
||||
key: keyof T;
|
||||
title?: string;
|
||||
format?: (obj: T) => string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TableProps<T> {
|
||||
rows: T[];
|
||||
columns: TableColumn<T>[];
|
||||
columns: { title?: string, key: keyof Partial<T>, className?: string }[];
|
||||
defaultOrderBy: keyof T;
|
||||
copyToClipboard?: keyof T;
|
||||
defaultOrderDir?: OrderDir;
|
||||
rowClasses?: (obj: T) => Record<string, boolean>;
|
||||
rowAction?: (ref: React.RefObject<HTMLElement>) => () => void;
|
||||
// TODO: Remove when pagination is implemented on the backend.
|
||||
paginationOffset: {
|
||||
startIndex: number;
|
||||
@@ -30,34 +22,12 @@ interface TableProps<T> {
|
||||
}
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const Table = <T extends TableRow>({
|
||||
rows,
|
||||
columns,
|
||||
defaultOrderBy,
|
||||
defaultOrderDir,
|
||||
copyToClipboard,
|
||||
paginationOffset,
|
||||
rowClasses,
|
||||
rowAction = (_ref: React.RefObject<HTMLElement>) => () => {},
|
||||
}: TableProps<T>) => {
|
||||
const Table = <T extends object>({ rows, columns, defaultOrderBy, defaultOrderDir, copyToClipboard, paginationOffset }: TableProps<T>) => {
|
||||
const handleCopyToClipboard = useCopyToClipboard();
|
||||
|
||||
const [orderBy, setOrderBy] = useState<keyof T>(defaultOrderBy);
|
||||
const [orderDir, setOrderDir] = useState<OrderDir>(defaultOrderDir || "desc");
|
||||
const [copied, setCopied] = useState<number | null>(null);
|
||||
const [rowRefs, setRowRefs] = useState(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
const newRowRefs = new Map();
|
||||
rows.forEach(row => {
|
||||
newRowRefs.set(row.id, createRef());
|
||||
});
|
||||
setRowRefs(newRowRefs);
|
||||
}, [rows]);
|
||||
|
||||
// const sortedList = useMemo(() => stableSort(rows as [], getComparator(orderDir, orderBy)),
|
||||
// [rows, orderBy, orderDir]);
|
||||
@@ -88,8 +58,6 @@ const Table = <T extends TableRow>({
|
||||
return () => clearTimeout(timeout);
|
||||
}, [copied]);
|
||||
|
||||
const copyCol = copyToClipboard && columns.find((col) => col.key == copyToClipboard);
|
||||
|
||||
return (
|
||||
<table className="vm-table">
|
||||
<thead className="vm-table-header">
|
||||
@@ -122,14 +90,8 @@ const Table = <T extends TableRow>({
|
||||
<tbody className="vm-table-body">
|
||||
{sortedList.map((row, rowIndex) => (
|
||||
<tr
|
||||
className={classNames({
|
||||
"vm-table__row": true,
|
||||
...(rowClasses ? rowClasses(row) : {}),
|
||||
})}
|
||||
id={row.id}
|
||||
className="vm-table__row"
|
||||
key={rowIndex}
|
||||
ref={rowRefs.get(row.id)}
|
||||
onClick={rowAction(rowRefs.get(row.id))}
|
||||
>
|
||||
{columns.map((col) => (
|
||||
<td
|
||||
@@ -139,10 +101,10 @@ const Table = <T extends TableRow>({
|
||||
})}
|
||||
key={String(col.key)}
|
||||
>
|
||||
{(col.format ? col.format(row) : String(row[col.key])) || "-"}
|
||||
{row[col.key] || "-"}
|
||||
</td>
|
||||
))}
|
||||
{copyToClipboard && copyCol && (
|
||||
{copyToClipboard && (
|
||||
<td className="vm-table-cell vm-table-cell_right">
|
||||
{row[copyToClipboard] && (
|
||||
<div className="vm-table-cell__content">
|
||||
@@ -152,7 +114,7 @@ const Table = <T extends TableRow>({
|
||||
color={copied === rowIndex ? "success" : "gray"}
|
||||
size="small"
|
||||
startIcon={copied === rowIndex ? <DoneIcon/> : <CopyIcon/>}
|
||||
onClick={createCopyHandler(copyCol.format ? copyCol.format(row) : String(row[copyToClipboard]), rowIndex)}
|
||||
onClick={createCopyHandler(row[copyToClipboard], rowIndex)}
|
||||
ariaLabel="copy row"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -41,12 +41,12 @@ export function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getComparator<T extends object>(
|
||||
export function getComparator<Key extends (string | number | symbol)>(
|
||||
order: Order,
|
||||
orderBy: keyof T,
|
||||
orderBy: Key,
|
||||
): (
|
||||
a: T,
|
||||
b: T,
|
||||
a: { [key in Key]: number | string },
|
||||
b: { [key in Key]: number | string },
|
||||
) => number {
|
||||
return order === "desc"
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
@@ -55,7 +55,7 @@ export function getComparator<T extends object>(
|
||||
|
||||
// This method is created for cross-browser compatibility, if you don't
|
||||
// need to support IE11, you can use Array.prototype.sort() directly
|
||||
export function stableSort<T>(array: readonly T[], comparator: (a: T, b: T) => number): T[] {
|
||||
export function stableSort<T>(array: readonly T[], comparator: (a: T, b: T) => number) {
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
|
||||
@@ -10,7 +10,6 @@ export const darkPalette = {
|
||||
"color-success": "#57ab5a",
|
||||
"color-background-success": "#0e1b0e",
|
||||
"color-passive": "#a7acb3",
|
||||
"color-code": "#4e5a6a",
|
||||
"color-background-body": "#22272e",
|
||||
"color-background-block": "#2d333b",
|
||||
"color-background-tooltip": "rgba(22, 22, 22, 0.8)",
|
||||
@@ -45,7 +44,6 @@ export const lightPalette = {
|
||||
"color-success": "#4caf50",
|
||||
"color-background-success": "#d4ecd5",
|
||||
"color-passive": "#5d6267",
|
||||
"color-code": "ecedee",
|
||||
"color-background-body": "#FEFEFF",
|
||||
"color-background-block": "#FFFFFF",
|
||||
"color-background-tooltip": "rgba(80,80,80,0.9)",
|
||||
|
||||
@@ -41,7 +41,7 @@ const Footer: FC<Props> = memo(({ links = footerLinksByDefault }) => {
|
||||
))}
|
||||
<div className="vm-footer__copyright">
|
||||
© {copyrightYears} VictoriaMetrics.
|
||||
{version && <span className="vm-footer__version"> Version: {version}</span>}
|
||||
{version && <span className="vm-footer__version"> {version}</span>}
|
||||
</div>
|
||||
</footer>;
|
||||
});
|
||||
|
||||
@@ -155,18 +155,15 @@ const ExploreRules: FC = () => {
|
||||
[groups, types, states, searchInput]
|
||||
);
|
||||
|
||||
const selectedTypes = allTypes.size === types.length ? [] : types;
|
||||
const selectedStates = allStates.size === states.length ? [] : states;
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalOpen && getModal()}
|
||||
{(!modalOpen || !!allStates?.size) && (
|
||||
<div className="vm-explore-alerts">
|
||||
<RulesHeader
|
||||
types={selectedTypes}
|
||||
types={types}
|
||||
allTypes={Array.from(allTypes)}
|
||||
states={selectedStates}
|
||||
states={states}
|
||||
allStates={Array.from(allStates)}
|
||||
onChangeTypes={handleChangeTypes}
|
||||
onChangeStates={handleChangeStates}
|
||||
|
||||
@@ -15,7 +15,7 @@ const TopQueryTable:FC<TopQueryPanelProps> = ({ rows, columns, defaultOrderBy })
|
||||
const [orderBy, setOrderBy] = useState<keyof TopQuery>(defaultOrderBy || "count");
|
||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("desc");
|
||||
|
||||
const sortedList = useMemo(() => stableSort(rows, getComparator(orderDir, orderBy)),
|
||||
const sortedList = useMemo(() => stableSort(rows as [], getComparator(orderDir, orderBy)) as TopQuery[],
|
||||
[rows, orderBy, orderDir]);
|
||||
|
||||
const onSortHandler = (key: keyof TopQuery) => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
&-fields {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-medium;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ $color-error-text: var(--color-error-text);
|
||||
$color-warning-text: var(--color-warning-text);
|
||||
$color-info-text: var(--color-info-text);
|
||||
$color-success-text: var(--color-success-text);
|
||||
$color-code: var(--color-code);
|
||||
|
||||
$color-text: var(--color-text);
|
||||
$color-text-secondary: var(--color-text-secondary);
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getDefaultURL } from "./default-server-url";
|
||||
|
||||
describe("test server urls", () => {
|
||||
describe("getDefaultURL()", () => {
|
||||
it("/select/0/vmui/", () => {
|
||||
const result = getDefaultURL("https://localhost:1111/select/0/vmui/");
|
||||
expect(result).toBe("https://localhost:1111/select/0/prometheus");
|
||||
});
|
||||
|
||||
it("/any/path/prefix/select/multitenant/vmui/#/rules?q=test", () => {
|
||||
const result = getDefaultURL("http://test/any/path/prefix/select/multitenant/vmui/#/rules?q=test");
|
||||
expect(result).toBe("http://test/any/path/prefix/select/multitenant/prometheus");
|
||||
});
|
||||
|
||||
it("/test/select/1:1/prometheus/graph/", () => {
|
||||
const result = getDefaultURL("https://domain.com/test/select/1:1/prometheus/graph/");
|
||||
expect(result).toBe("https://domain.com/test/select/1:1/prometheus");
|
||||
});
|
||||
|
||||
it("https://play.vm.com/#/rules?q=test", () => {
|
||||
const result = getDefaultURL("https://play.vm.com/#/rules?q=test");
|
||||
expect(result).toBe("https://play.vm.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,15 +3,12 @@ import { replaceTenantId } from "./tenants";
|
||||
import { APP_TYPE, AppType } from "../constants/appType";
|
||||
import { getFromStorage } from "./storage";
|
||||
|
||||
export const getDefaultURL = (u: string) => {
|
||||
return u.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "").replace(/(\/select\/[^/]+)$/, "$1/prometheus");
|
||||
};
|
||||
|
||||
export const getDefaultServer = (tenantId?: string): string => {
|
||||
const { serverURL } = getAppModeParams();
|
||||
const storageURL = getFromStorage("SERVER_URL") as string;
|
||||
const anomalyURL = `${window.location.origin}${window.location.pathname.replace(/^\/vmui/, "")}`;
|
||||
const defaultURL = getDefaultURL(window.location.href);
|
||||
const baseURL = window.location.href.replace(/(\/(?:prometheus\/)?(?:graph|vmui)\/.*|\/#\/.*)/, "");
|
||||
const defaultURL = baseURL.replace(/(\/select\/[\d:]+)$/, "$1/prometheus");
|
||||
const url = serverURL || storageURL || defaultURL;
|
||||
|
||||
switch (APP_TYPE) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const regexp = /(\/select\/)([^/])(\/)(.+)/;
|
||||
const regexp = /(\/select\/)(\d+|\d.+)(\/)(.+)/;
|
||||
|
||||
export const replaceTenantId = (serverUrl: string, tenantId: string) => {
|
||||
return serverUrl.replace(regexp, `$1${tenantId}/$4`);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package apptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -500,3 +503,44 @@ func sortTSDBStatusResponseEntries(entries []TSDBStatusResponseEntry) {
|
||||
return left.Count < right.Count
|
||||
})
|
||||
}
|
||||
|
||||
// LogsQLQueryResponse is an in-memory representation of the
|
||||
// /select/logsql/query response.
|
||||
type LogsQLQueryResponse struct {
|
||||
LogLines []string
|
||||
}
|
||||
|
||||
// NewLogsQLQueryResponse is a test helper function that creates a new
|
||||
// instance of LogsQLQueryResponse by unmarshalling a json string.
|
||||
func NewLogsQLQueryResponse(t *testing.T, s string) *LogsQLQueryResponse {
|
||||
t.Helper()
|
||||
res := &LogsQLQueryResponse{}
|
||||
if len(s) == 0 {
|
||||
return res
|
||||
}
|
||||
bs := bytes.NewBufferString(s)
|
||||
for {
|
||||
logLine, err := bs.ReadString('\n')
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
if len(logLine) > 0 {
|
||||
t.Fatalf("BUG: unexpected non-empty line=%q with io.EOF", logLine)
|
||||
}
|
||||
break
|
||||
}
|
||||
t.Fatalf("BUG: cannot read logline from buffer: %s", err)
|
||||
}
|
||||
var lv map[string]any
|
||||
if err := json.Unmarshal([]byte(logLine), &lv); err != nil {
|
||||
t.Fatalf("cannot parse log line=%q: %s", logLine, err)
|
||||
}
|
||||
delete(lv, "_stream_id")
|
||||
normalizedLine, err := json.Marshal(lv)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot marshal parsed logline=%q: %s", logLine, err)
|
||||
}
|
||||
res.LogLines = append(res.LogLines, string(normalizedLine))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ func testSpecialQueryRegression(tc *apptest.TestCase, sut apptest.PrometheusWrit
|
||||
testTooBigLookbehindWindow(tc, sut)
|
||||
testMatchSeries(tc, sut)
|
||||
testNegativeIncrease(tc, sut)
|
||||
testInstantQueryWithOffsetUsingCache(tc, sut)
|
||||
|
||||
// graphite
|
||||
testComparisonNotInfNotNan(tc, sut)
|
||||
@@ -293,45 +292,6 @@ func testNegativeIncrease(tc *apptest.TestCase, sut apptest.PrometheusWriteQueri
|
||||
})
|
||||
}
|
||||
|
||||
func testInstantQueryWithOffsetUsingCache(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier) {
|
||||
t := tc.T()
|
||||
|
||||
// unexpected /api/v1/query response due to wrong applied offset to request range
|
||||
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9762
|
||||
sut.PrometheusAPIV1ImportPrometheus(t, []string{
|
||||
`vm_http_requests_total 1 1758196800000`, // 2025-09-18 12:00:00
|
||||
`vm_http_requests_total 2 1758218400000`, // 2025-09-18 18:00:00
|
||||
`vm_http_requests_total 3 1758240000000`, // 2025-09-19 00:00:00
|
||||
`vm_http_requests_total 4 1758261600000`, // 2025-09-19 06:00:00
|
||||
`vm_http_requests_total 5 1758283200000`, // 2025-09-19 12:00:00
|
||||
`vm_http_requests_total 6 1758304800000`, // 2025-09-19 18:00:00
|
||||
`vm_http_requests_total 7 1758326400000`, // 2025-09-20 00:00:00
|
||||
}, apptest.QueryOpts{})
|
||||
sut.ForceFlush(t)
|
||||
|
||||
tc.Assert(&apptest.AssertOptions{
|
||||
Msg: "unexpected /api/v1/query response",
|
||||
DoNotRetry: true,
|
||||
Got: func() any {
|
||||
return sut.PrometheusAPIV1Query(t, `avg_over_time(vm_http_requests_total[1d] offset 12h)`, apptest.QueryOpts{
|
||||
Time: "2025-09-20T12:00:00.000Z",
|
||||
})
|
||||
},
|
||||
Want: &apptest.PrometheusAPIV1QueryResponse{
|
||||
Status: "success",
|
||||
Data: &apptest.QueryData{
|
||||
ResultType: "vector",
|
||||
Result: []*apptest.QueryResult{
|
||||
{
|
||||
Metric: map[string]string{},
|
||||
Sample: &apptest.Sample{Timestamp: 1758369600000, Value: 5.5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testComparisonNotInfNotNan(tc *apptest.TestCase, sut apptest.PrometheusWriteQuerier) {
|
||||
t := tc.T()
|
||||
|
||||
|
||||
@@ -1786,4 +1786,4 @@
|
||||
"uid": "gF-lxRdVz",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
}
|
||||
@@ -1168,7 +1168,7 @@
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (le, controller) )",
|
||||
"expr": "histogram_quantile(0.99,sum(rate(controller_runtime_reconcile_time_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by(le,controller) )",
|
||||
"legendFormat": "q.99 {{controller}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
@@ -1265,7 +1265,7 @@
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum(rate(rest_client_requests_total{job=~\"$job\"}[$__interval])) by (method, code)",
|
||||
"expr": "sum(rate(rest_client_requests_total{job=~\"$job\"}[$__interval])) by (method,code)",
|
||||
"instant": false,
|
||||
"legendFormat": "{{method}} {{code}}",
|
||||
"range": true,
|
||||
@@ -1489,7 +1489,7 @@
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "max(histogram_quantile(0.99, sum(rate(go_sched_latencies_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (job, instance, le))) by (job)",
|
||||
"expr": "max(histogram_quantile(0.99, sum(rate(go_sched_latencies_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (job, instance, le))) by(job)",
|
||||
"instant": false,
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
@@ -1588,7 +1588,7 @@
|
||||
"uid": "$ds"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "histogram_quantile(0.99, sum(rate(rest_client_request_duration_seconds_bucket{job=~\"$job\"}[$__rate_interval])) by (le, method, api))",
|
||||
"expr": "histogram_quantile(0.99,sum(rate(rest_client_request_duration_seconds_bucket{job=~\"$job\"})) by(le,method,api) )",
|
||||
"instant": false,
|
||||
"legendFormat": "{{method}} {{api}}",
|
||||
"range": true,
|
||||
@@ -2135,16 +2135,6 @@
|
||||
"skipUrlSync": false,
|
||||
"sort": 2,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"baseFilters": [],
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"filters": [],
|
||||
"name": "adhoc",
|
||||
"type": "adhoc"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1950,16 +1950,6 @@
|
||||
],
|
||||
"query": "*",
|
||||
"type": "textbox"
|
||||
},
|
||||
{
|
||||
"baseFilters": [],
|
||||
"datasource": {
|
||||
"type": "victoriametrics-logs-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"filters": [],
|
||||
"name": "adhoc",
|
||||
"type": "adhoc"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1972,4 +1962,4 @@
|
||||
"title": "Query Stats (cluster)",
|
||||
"uid": "feg3od1zt1fy8e",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -1787,4 +1787,4 @@
|
||||
"uid": "gF-lxRdVz_vm",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
}
|
||||
@@ -1994,7 +1994,7 @@
|
||||
"baseFilters": [],
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
"uid": "PE8D8DB4BEE4E4B22"
|
||||
},
|
||||
"filters": [],
|
||||
"name": "adhoc",
|
||||
|
||||
@@ -2136,16 +2136,6 @@
|
||||
"skipUrlSync": false,
|
||||
"sort": 2,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"baseFilters": [],
|
||||
"datasource": {
|
||||
"type": "victoriametrics-metrics-datasource",
|
||||
"uid": "$ds"
|
||||
},
|
||||
"filters": [],
|
||||
"name": "adhoc",
|
||||
"type": "adhoc"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -4238,4 +4238,4 @@
|
||||
"title": "VictoriaMetrics - vmalert (VM)",
|
||||
"uid": "LzldHAVnz_vm",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -2652,7 +2652,7 @@
|
||||
{
|
||||
"datasource": {
|
||||
"type": "victoriametrics-datasource",
|
||||
"uid": "$ds"
|
||||
"uid": "P38648FE0F8C5BEA2"
|
||||
},
|
||||
"filters": [],
|
||||
"hide": 0,
|
||||
|
||||
@@ -7,7 +7,7 @@ ROOT_IMAGE ?= alpine:3.22.1
|
||||
ROOT_IMAGE_SCRATCH ?= scratch
|
||||
CERTS_IMAGE := alpine:3.22.1
|
||||
|
||||
GO_BUILDER_IMAGE := golang:1.25.1
|
||||
GO_BUILDER_IMAGE := golang:1.25.0
|
||||
|
||||
BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr :/ __)-1
|
||||
BASE_IMAGE := local/base:1.1.4-$(shell echo $(ROOT_IMAGE) | tr :/ __)-$(shell echo $(CERTS_IMAGE) | tr :/ __)
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# It scrapes targets defined in --promscrape.config
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.126.0
|
||||
image: victoriametrics/vmagent:v1.125.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.2.0
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
@@ -37,14 +37,14 @@ services:
|
||||
# vmstorage shards. Each shard receives 1/N of all metrics sent to vminserts,
|
||||
# where N is number of vmstorages (2 in this case).
|
||||
vmstorage-1:
|
||||
image: victoriametrics/vmstorage:v1.126.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.125.1-cluster
|
||||
volumes:
|
||||
- strgdata-1:/storage
|
||||
command:
|
||||
- "--storageDataPath=/storage"
|
||||
restart: always
|
||||
vmstorage-2:
|
||||
image: victoriametrics/vmstorage:v1.126.0-cluster
|
||||
image: victoriametrics/vmstorage:v1.125.1-cluster
|
||||
volumes:
|
||||
- strgdata-2:/storage
|
||||
command:
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
# vminsert is ingestion frontend. It receives metrics pushed by vmagent,
|
||||
# pre-process them and distributes across configured vmstorage shards.
|
||||
vminsert-1:
|
||||
image: victoriametrics/vminsert:v1.126.0-cluster
|
||||
image: victoriametrics/vminsert:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -63,7 +63,7 @@ services:
|
||||
- "--storageNode=vmstorage-2:8400"
|
||||
restart: always
|
||||
vminsert-2:
|
||||
image: victoriametrics/vminsert:v1.126.0-cluster
|
||||
image: victoriametrics/vminsert:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
# vmselect is a query fronted. It serves read queries in MetricsQL or PromQL.
|
||||
# vmselect collects results from configured `--storageNode` shards.
|
||||
vmselect-1:
|
||||
image: victoriametrics/vmselect:v1.126.0-cluster
|
||||
image: victoriametrics/vmselect:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
- "--vmalert.proxyURL=http://vmalert:8880"
|
||||
restart: always
|
||||
vmselect-2:
|
||||
image: victoriametrics/vmselect:v1.126.0-cluster
|
||||
image: victoriametrics/vmselect:v1.125.1-cluster
|
||||
depends_on:
|
||||
- "vmstorage-1"
|
||||
- "vmstorage-2"
|
||||
@@ -100,7 +100,7 @@ services:
|
||||
# read requests from Grafana, vmui, vmalert among vmselects.
|
||||
# It can be used as an authentication proxy.
|
||||
vmauth:
|
||||
image: victoriametrics/vmauth:v1.126.0
|
||||
image: victoriametrics/vmauth:v1.125.1
|
||||
depends_on:
|
||||
- "vmselect-1"
|
||||
- "vmselect-2"
|
||||
@@ -114,7 +114,7 @@ services:
|
||||
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.126.0
|
||||
image: victoriametrics/vmalert:v1.125.1
|
||||
depends_on:
|
||||
- "vmauth"
|
||||
ports:
|
||||
@@ -138,7 +138,7 @@ services:
|
||||
# alertmanager receives alerting notifications from vmalert
|
||||
# and distributes them according to --config.file.
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.28.1
|
||||
image: prom/alertmanager:v0.28.0
|
||||
volumes:
|
||||
- ./alertmanager.yml:/config/alertmanager.yml
|
||||
command:
|
||||
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
# It scrapes targets defined in --promscrape.config
|
||||
# And forward them to --remoteWrite.url
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.126.0
|
||||
image: victoriametrics/vmagent:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
# VictoriaMetrics instance, a single process responsible for
|
||||
# storing metrics and serve read requests.
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.126.0
|
||||
image: victoriametrics/victoria-metrics:v1.125.1
|
||||
ports:
|
||||
- 8428:8428
|
||||
- 8089:8089
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.2.0
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
|
||||
# vmalert executes alerting and recording rules
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.126.0
|
||||
image: victoriametrics/vmalert:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
- "alertmanager"
|
||||
@@ -79,7 +79,7 @@ services:
|
||||
# alertmanager receives alerting notifications from vmalert
|
||||
# and distributes them according to --config.file.
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.28.1
|
||||
image: prom/alertmanager:v0.28.0
|
||||
volumes:
|
||||
- ./alertmanager.yml:/config/alertmanager.yml
|
||||
command:
|
||||
|
||||
15
deployment/docker/vlogs-example-alerts.yml
Normal file
15
deployment/docker/vlogs-example-alerts.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
groups:
|
||||
- name: log-rules
|
||||
type: vlogs
|
||||
interval: 30s
|
||||
rules:
|
||||
- alert: AlwaysFiring
|
||||
expr: '* | stats count()'
|
||||
annotations:
|
||||
description: "Generated more than {{$value}} log entries in the last 1 minute"
|
||||
- alert: TooManyLogs
|
||||
expr: '* | stats by (path) count() as total | filter total:>50'
|
||||
annotations:
|
||||
description: "Path {{$labels.path}} generated more than 50 log entries in the last 1 minute: {{$value}}"
|
||||
- record: path:logs:count
|
||||
expr: '* | stats by (path) count()'
|
||||
@@ -1,6 +1,6 @@
|
||||
# Docker Compose file for "vmanomaly integration" guide
|
||||
|
||||
Please read the "vmanomaly integration" guide first - [Anomaly Detection and Alerting Setup](https://docs.victoriametrics.com/anomaly-detection/guides/guide-vmanomaly-vmalert/)
|
||||
Please read the "vmanomaly integration" guide first - [https://docs.victoriametrics.com/anomaly-detection/guides/guide-vmanomaly-vmalert.html](https://docs.victoriametrics.com/anomaly-detection/guides/guide-vmanomaly-vmalert.html)
|
||||
|
||||
To make this Docker compose file work, you MUST replace the content of [vmanomaly_license](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/master/deployment/docker/vmanomaly/vmanomaly-integration/vmanomaly_license) with valid license.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
vmagent:
|
||||
image: victoriametrics/vmagent:v1.126.0
|
||||
image: victoriametrics/vmagent:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
restart: always
|
||||
|
||||
victoriametrics:
|
||||
image: victoriametrics/victoria-metrics:v1.126.0
|
||||
image: victoriametrics/victoria-metrics:v1.125.1
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.2.0
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
restart: always
|
||||
|
||||
vmalert:
|
||||
image: victoriametrics/vmalert:v1.126.0
|
||||
image: victoriametrics/vmalert:v1.125.1
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
- "/config.yaml"
|
||||
- "--licenseFile=/license"
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.28.1
|
||||
image: prom/alertmanager:v0.28.0
|
||||
volumes:
|
||||
- ./alertmanager.yml:/config/alertmanager.yml
|
||||
command:
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
restart: always
|
||||
|
||||
node-exporter:
|
||||
image: quay.io/prometheus/node-exporter:v1.9.1
|
||||
image: quay.io/prometheus/node-exporter:v1.7.0
|
||||
ports:
|
||||
- 9100:9100
|
||||
pid: host
|
||||
|
||||
17
deployment/logs-benchmark/Makefile
Normal file
17
deployment/logs-benchmark/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
prepare-logs:
|
||||
cd ./source_logs && bash download.sh
|
||||
|
||||
docker-up-elk:
|
||||
docker-compose -f docker-compose.yml -f docker-compose-elk.yml up -d
|
||||
|
||||
docker-stop-elk:
|
||||
docker-compose -f docker-compose.yml -f docker-compose-elk.yml stop
|
||||
|
||||
docker-up-loki:
|
||||
docker-compose -f docker-compose.yml -f docker-compose-loki.yml up -d
|
||||
|
||||
docker-stop-loki:
|
||||
docker-compose -f docker-compose.yml -f docker-compose-loki.yml stop
|
||||
|
||||
docker-cleanup:
|
||||
docker-compose -f docker-compose.yml -f docker-compose-elk.yml -f docker-compose-loki.yml down -v --remove-orphans
|
||||
69
deployment/logs-benchmark/docker-compose-elk.yml
Normal file
69
deployment/logs-benchmark/docker-compose-elk.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
filebeat-elastic:
|
||||
image: docker.elastic.co/beats/filebeat:8.8.0
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ./elk/filebeat/filebeat-elastic.yml:/usr/share/filebeat/filebeat.yml:ro
|
||||
depends_on:
|
||||
- elastic
|
||||
|
||||
filebeat-vlogs:
|
||||
image: docker.elastic.co/beats/filebeat:8.8.0
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ./elk/filebeat/filebeat-vlogs.yml:/usr/share/filebeat/filebeat.yml:ro
|
||||
depends_on:
|
||||
- vlogs
|
||||
|
||||
generator:
|
||||
image: golang:1.25.0-alpine
|
||||
restart: always
|
||||
working_dir: /go/src/app
|
||||
volumes:
|
||||
- ./generator:/go/src/app
|
||||
- ./source_logs:/go/src/source_logs
|
||||
command:
|
||||
- go
|
||||
- run
|
||||
- main.go
|
||||
- -logsPath=/go/src/source_logs/logs
|
||||
- -outputRateLimitItems=10000
|
||||
- -syslog.addr=filebeat-elastic:12345
|
||||
- -syslog.addr2=filebeat-vlogs:12345
|
||||
- -logs.randomSuffix=false
|
||||
depends_on: [filebeat-elastic, filebeat-vlogs]
|
||||
|
||||
elastic:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
|
||||
volumes:
|
||||
- ./elk/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
|
||||
- elastic:/usr/share/elasticsearch/data
|
||||
environment:
|
||||
ES_JAVA_OPTS: "-Xmx2048m"
|
||||
|
||||
kibana:
|
||||
image: docker.elastic.co/kibana/kibana:8.8.0
|
||||
volumes:
|
||||
- ./elk/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml
|
||||
ports:
|
||||
- "5601:5601"
|
||||
depends_on: [elastic]
|
||||
|
||||
beat-exporter-elastic:
|
||||
image: trustpilot/beat-exporter:0.4.0
|
||||
command:
|
||||
- -beat.uri=http://filebeat-elastic:5066
|
||||
depends_on:
|
||||
- filebeat-elastic
|
||||
|
||||
beat-exporter-vlogs:
|
||||
image: trustpilot/beat-exporter:0.4.0
|
||||
command:
|
||||
- -beat.uri=http://filebeat-vlogs:5066
|
||||
depends_on:
|
||||
- filebeat-vlogs
|
||||
|
||||
volumes:
|
||||
elastic:
|
||||
51
deployment/logs-benchmark/docker-compose-loki.yml
Normal file
51
deployment/logs-benchmark/docker-compose-loki.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
generator:
|
||||
image: golang:1.25.0-alpine
|
||||
restart: always
|
||||
working_dir: /go/src/app
|
||||
volumes:
|
||||
- ./generator:/go/src/app
|
||||
- ./source_logs:/go/src/source_logs
|
||||
command:
|
||||
- go
|
||||
- run
|
||||
- main.go
|
||||
- -logsPath=/go/src/source_logs/logs
|
||||
- -outputRateLimitItems=10000
|
||||
- -outputRateLimitPeriod=1s
|
||||
- -syslog.addr=rsyslog:514
|
||||
- -syslog.addr2=rsyslog:514
|
||||
- -logs.randomSuffix=false
|
||||
depends_on: [rsyslog]
|
||||
|
||||
loki:
|
||||
image: grafana/loki:2.9.0
|
||||
user: 0:0
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/loki-config.yaml
|
||||
volumes:
|
||||
- loki:/tmp/loki
|
||||
- ./loki/:/etc/loki/
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:2.9.0
|
||||
command: -config.file=/etc/promtail/promtail-config.yaml
|
||||
volumes:
|
||||
- ./loki/:/etc/promtail/
|
||||
depends_on:
|
||||
- loki
|
||||
- vlogs
|
||||
|
||||
rsyslog:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: rsyslog
|
||||
volumes:
|
||||
- ./rsyslog/rsyslog.conf:/etc/rsyslog.conf
|
||||
depends_on: [promtail]
|
||||
|
||||
volumes:
|
||||
loki:
|
||||
74
deployment/logs-benchmark/docker-compose.yml
Normal file
74
deployment/logs-benchmark/docker-compose.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
# Run `make package-victoria-logs` to build victoria-logs image
|
||||
vlogs:
|
||||
image: docker.io/victoriametrics/victoria-logs:v1.24.0-victorialogs
|
||||
volumes:
|
||||
- vlogs:/vlogs
|
||||
ports:
|
||||
- "9428:9428"
|
||||
command:
|
||||
- -storageDataPath=/vlogs
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:v0.47.0
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
- /dev/disk/:/dev/disk:ro
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- "--path.procfs=/host/proc"
|
||||
- "--path.rootfs=/rootfs"
|
||||
- "--path.sysfs=/host/sys"
|
||||
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
|
||||
|
||||
du-exporter:
|
||||
image: ghcr.io/dundee/disk_usage_exporter/disk_usage_exporter-c4084307c537335c2ddb6f4b9b527422:latest
|
||||
restart: unless-stopped
|
||||
user: "root"
|
||||
volumes:
|
||||
- /var/lib/docker/volumes:/var/lib/docker/volumes:ro
|
||||
- ./du/config.yml:/config.yml:ro
|
||||
command:
|
||||
- "--config=/config.yml"
|
||||
|
||||
vmsingle:
|
||||
image: victoriametrics/victoria-metrics:v1.109.0
|
||||
ports:
|
||||
- "8428:8428"
|
||||
command:
|
||||
- -storageDataPath=/vmsingle
|
||||
- -promscrape.config=/promscrape.yml
|
||||
- -promscrape.maxScrapeSize=1Gb
|
||||
volumes:
|
||||
- vmsingle:/vmsingle
|
||||
- ./vmsingle/promscrape.yml:/promscrape.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.1.1
|
||||
depends_on: [vmsingle]
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- grafanadata:/var/lib/grafana
|
||||
- ./grafana/provisioning/:/etc/grafana/provisioning/
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards/
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
vlogs:
|
||||
vmsingle:
|
||||
grafanadata: {}
|
||||
3
deployment/logs-benchmark/du/config.yml
Normal file
3
deployment/logs-benchmark/du/config.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
analyzed-path: /var/lib/docker/volumes
|
||||
bind-address: 0.0.0.0:9995
|
||||
dir-level: 1
|
||||
4
deployment/logs-benchmark/elk/elastic/elasticsearch.yml
Normal file
4
deployment/logs-benchmark/elk/elastic/elasticsearch.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
cluster.name: "bench"
|
||||
network.host: 0.0.0.0
|
||||
xpack.security.enabled: false
|
||||
discovery.type: single-node
|
||||
15
deployment/logs-benchmark/elk/filebeat/filebeat-elastic.yml
Normal file
15
deployment/logs-benchmark/elk/filebeat/filebeat-elastic.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
filebeat.inputs:
|
||||
- type: syslog
|
||||
format: rfc3164
|
||||
protocol.tcp:
|
||||
host: "0.0.0.0:12345"
|
||||
|
||||
output.elasticsearch:
|
||||
hosts: [ "http://elastic:9200" ]
|
||||
worker: 5
|
||||
bulk_max_size: 1000
|
||||
|
||||
http:
|
||||
enabled: true
|
||||
host: 0.0.0.0
|
||||
port: 5066
|
||||
19
deployment/logs-benchmark/elk/filebeat/filebeat-vlogs.yml
Normal file
19
deployment/logs-benchmark/elk/filebeat/filebeat-vlogs.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
filebeat.inputs:
|
||||
- type: syslog
|
||||
format: rfc3164
|
||||
protocol.tcp:
|
||||
host: "0.0.0.0:12345"
|
||||
|
||||
output.elasticsearch:
|
||||
hosts: [ "http://vlogs:9428/insert/elasticsearch/" ]
|
||||
worker: 5
|
||||
bulk_max_size: 1000
|
||||
parameters:
|
||||
_msg_field: "message"
|
||||
_time_field: "@timestamp"
|
||||
_stream_fields: "host.name,process.program,process.pid"
|
||||
|
||||
http:
|
||||
enabled: true
|
||||
host: 0.0.0.0
|
||||
port: 5066
|
||||
3
deployment/logs-benchmark/elk/kibana/kibana.yml
Normal file
3
deployment/logs-benchmark/elk/kibana/kibana.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
server.name: kibana
|
||||
server.host: "0.0.0.0"
|
||||
elasticsearch.hosts: [ "http://elastic:9200" ]
|
||||
106
deployment/logs-benchmark/generator/main.go
Normal file
106
deployment/logs-benchmark/generator/main.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/syslog"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
logsPath = flag.String("logsPath", "", "Path to logs directory")
|
||||
syslogAddr = flag.String("syslog.addr", "logstash:12345", "Addr to send logs to")
|
||||
syslogAddr2 = flag.String("syslog.addr2", "logstash:12345", "Addr to send logs to")
|
||||
randomSuffix = flag.Bool("logs.randomSuffix", false, "Whether to add a random suffix to a log line")
|
||||
|
||||
outputRateLimitItems = flag.Int("outputRateLimitItems", 100, "Number of items to send per second")
|
||||
outputRateLimitPeriod = flag.Duration("outputRateLimitPeriod", time.Second, "Period of time to send items")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
startedAt := time.Now().Unix()
|
||||
|
||||
logFiles, err := os.ReadDir(*logsPath)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error reading directory %s:%w", *logsPath, err))
|
||||
}
|
||||
|
||||
sourceFiles := make([]string, 0)
|
||||
|
||||
for _, logFile := range logFiles {
|
||||
if strings.HasSuffix(logFile.Name(), ".log") {
|
||||
sourceFiles = append(sourceFiles, logFile.Name())
|
||||
}
|
||||
}
|
||||
log.Printf("sourceFiles: %v", sourceFiles)
|
||||
log.Printf("running with rate limit: %d items per %s", *outputRateLimitItems, *outputRateLimitPeriod)
|
||||
|
||||
limitTicker := time.NewTicker(*outputRateLimitPeriod)
|
||||
limitItems := *outputRateLimitItems
|
||||
limiter := make(chan struct{}, limitItems)
|
||||
go func() {
|
||||
for {
|
||||
<-limitTicker.C
|
||||
for i := 0; i < limitItems; i++ {
|
||||
limiter <- struct{}{}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, sourceFile := range sourceFiles {
|
||||
log.Printf("sourceFile: %s", sourceFile)
|
||||
f, err := os.Open(*logsPath + "/" + sourceFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
syslogTag := "logs-benchmark-" + sourceFile + "-" + strconv.FormatInt(startedAt, 10)
|
||||
|
||||
// Loki uses RFC5424 syslog format, which has a 48 character limit on the tag.
|
||||
tagLen := len(syslogTag)
|
||||
if tagLen > 48 {
|
||||
truncate := tagLen - 48
|
||||
syslogTag = syslogTag[truncate:]
|
||||
}
|
||||
logger, err := syslog.Dial("tcp", *syslogAddr, syslog.LOG_INFO, syslogTag)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error dialing syslog: %w", err))
|
||||
}
|
||||
logger2, err := syslog.Dial("tcp", *syslogAddr2, syslog.LOG_INFO, syslogTag)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("error dialing syslog: %w", err))
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
<-limiter
|
||||
line := scanner.Text()
|
||||
if *randomSuffix {
|
||||
line = line + " " + randomString()
|
||||
}
|
||||
_ = logger.Info(line)
|
||||
_ = logger2.Info(line)
|
||||
}
|
||||
|
||||
logger.Close()
|
||||
logger2.Close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func randomString() string {
|
||||
buf := make([]byte, 4)
|
||||
ip := rand.Uint32()
|
||||
|
||||
binary.LittleEndian.PutUint32(buf, ip)
|
||||
return net.IP(buf).String()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user