mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
fix(backend): refactor autocomplete
This commit is contained in:
committed by
Łukasz Mierzwa
parent
1cf2e5760f
commit
22847e24be
@@ -62,10 +62,11 @@ type Alertmanager struct {
|
||||
// CORS credentials
|
||||
CORSCredentials string `json:"corsCredentials"`
|
||||
// fields for storing pulled data
|
||||
alertGroups []models.AlertGroup
|
||||
autocomplete []models.Autocomplete
|
||||
knownLabels []string
|
||||
RequestTimeout time.Duration `json:"timeout"`
|
||||
alertGroups []models.AlertGroup
|
||||
autocomplete []models.Autocomplete
|
||||
autocompleteMap map[string]models.Autocomplete
|
||||
knownLabels []string
|
||||
RequestTimeout time.Duration `json:"timeout"`
|
||||
// lock protects data access while updating
|
||||
lock sync.RWMutex
|
||||
// whenever this instance should be proxied
|
||||
@@ -238,7 +239,11 @@ func (am *Alertmanager) pullAlerts(version string) error {
|
||||
|
||||
dedupedGroups := make([]models.AlertGroup, 0, len(uniqueGroups))
|
||||
colors := models.LabelsColorMap{}
|
||||
autocompleteMap := map[string]*models.Autocomplete{}
|
||||
if am.autocompleteMap == nil {
|
||||
am.autocompleteMap = map[string]models.Autocomplete{}
|
||||
} else {
|
||||
clear(am.autocompleteMap)
|
||||
}
|
||||
expiredSilences := am.ExpiredSilences()
|
||||
|
||||
log.Info().
|
||||
@@ -310,12 +315,8 @@ func (am *Alertmanager) pullAlerts(version string) error {
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
|
||||
for _, hint := range filters.BuildAutocomplete(alerts) {
|
||||
autocompleteMap[hint.Value] = &hint
|
||||
}
|
||||
for _, hint := range filters.LabelAutocomplete(labelPairs) {
|
||||
autocompleteMap[hint.Value] = &hint
|
||||
}
|
||||
filters.BuildAutocomplete(alerts, am.autocompleteMap)
|
||||
filters.LabelAutocomplete(labelPairs, am.autocompleteMap)
|
||||
|
||||
slices.SortFunc(alerts, models.CompareAlerts)
|
||||
ag.Alerts = alerts
|
||||
@@ -328,11 +329,11 @@ func (am *Alertmanager) pullAlerts(version string) error {
|
||||
|
||||
log.Info().
|
||||
Str("alertmanager", am.Name).
|
||||
Int("hints", len(autocompleteMap)).
|
||||
Int("hints", len(am.autocompleteMap)).
|
||||
Msg("Merging autocomplete hints")
|
||||
autocomplete := make([]models.Autocomplete, 0, len(autocompleteMap))
|
||||
for _, hint := range autocompleteMap {
|
||||
autocomplete = append(autocomplete, *hint)
|
||||
autocomplete := make([]models.Autocomplete, 0, len(am.autocompleteMap))
|
||||
for _, hint := range am.autocompleteMap {
|
||||
autocomplete = append(autocomplete, hint)
|
||||
}
|
||||
|
||||
knownLabels := make([]string, 0, len(knownLabelsMap))
|
||||
|
||||
@@ -4,27 +4,26 @@ import (
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
)
|
||||
|
||||
type autocompleteFactory func(name string, operators []string, alerts []models.Alert) []models.Autocomplete
|
||||
type autocompleteFactory func(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete)
|
||||
|
||||
func makeAC(value string, tokens []string) models.Autocomplete {
|
||||
func setAC(dst map[string]models.Autocomplete, value string, tokens []string) {
|
||||
if _, ok := dst[value]; ok {
|
||||
return
|
||||
}
|
||||
t := make([]string, len(tokens)+1)
|
||||
copy(t, tokens)
|
||||
t[len(tokens)] = value
|
||||
acHint := models.Autocomplete{
|
||||
dst[value] = models.Autocomplete{
|
||||
Value: value,
|
||||
Tokens: t,
|
||||
}
|
||||
return acHint
|
||||
}
|
||||
|
||||
// BuildAutocomplete takes an alert object and generates list of autocomplete
|
||||
// strings for it
|
||||
func BuildAutocomplete(alerts []models.Alert) []models.Autocomplete {
|
||||
acHints := []models.Autocomplete{}
|
||||
// BuildAutocomplete takes an alert list and populates dst with autocomplete hints.
|
||||
func BuildAutocomplete(alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, filterConfig := range AllFilters {
|
||||
if filterConfig.Autocomplete != nil {
|
||||
acHints = append(acHints, filterConfig.Autocomplete(filterConfig.Label, filterConfig.SupportedOperators, alerts)...)
|
||||
filterConfig.Autocomplete(filterConfig.Label, filterConfig.SupportedOperators, alerts, dst)
|
||||
}
|
||||
}
|
||||
return acHints
|
||||
}
|
||||
|
||||
@@ -136,11 +136,12 @@ func TestBuildAutocomplete(t *testing.T) {
|
||||
labelPairs = append(labelPairs, pairs)
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(acTest.Alerts))
|
||||
for _, hint := range filters.BuildAutocomplete(acTest.Alerts) {
|
||||
result = append(result, hint.Value)
|
||||
}
|
||||
for _, hint := range filters.LabelAutocomplete(labelPairs) {
|
||||
dst := map[string]models.Autocomplete{}
|
||||
filters.BuildAutocomplete(acTest.Alerts, dst)
|
||||
filters.LabelAutocomplete(labelPairs, dst)
|
||||
|
||||
result := make([]string, 0, len(dst))
|
||||
for _, hint := range dst {
|
||||
result = append(result, hint.Value)
|
||||
}
|
||||
|
||||
@@ -174,9 +175,21 @@ func BenchmarkAutocomplete(b *testing.B) {
|
||||
})
|
||||
labelPairs = append(labelPairs, pairs)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filters.BuildAutocomplete(alerts)
|
||||
filters.LabelAutocomplete(labelPairs)
|
||||
}
|
||||
b.Run("fresh", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
dst := map[string]models.Autocomplete{}
|
||||
filters.BuildAutocomplete(alerts, dst)
|
||||
filters.LabelAutocomplete(labelPairs, dst)
|
||||
}
|
||||
})
|
||||
b.Run("reuse", func(b *testing.B) {
|
||||
dst := map[string]models.Autocomplete{}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
clear(dst)
|
||||
filters.BuildAutocomplete(alerts, dst)
|
||||
filters.LabelAutocomplete(labelPairs, dst)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,25 +60,17 @@ func newAgeFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func ageAutocomplete(name string, operators []string, _ []models.Alert) []models.Autocomplete {
|
||||
tokens := make([]models.Autocomplete, 0, len(operators)*2)
|
||||
func ageAutocomplete(name string, operators []string, _ []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, operator := range operators {
|
||||
tokens = append(tokens, makeAC(
|
||||
name+operator+"10m",
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
))
|
||||
tokens = append(tokens, makeAC(
|
||||
name+operator+"1h",
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
))
|
||||
setAC(dst, name+operator+"10m", []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
setAC(dst, name+operator+"1h", []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
@@ -53,32 +53,20 @@ func newAlertmanagerInstanceFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func alertmanagerInstanceAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func alertmanagerInstanceAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
for _, am := range alert.Alertmanager {
|
||||
for _, operator := range operators {
|
||||
switch operator {
|
||||
case equalOperator, notEqualOperator:
|
||||
token := name + operator + am.Name
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -53,32 +53,20 @@ func newAlertmanagerClusterFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func alertmanagerClusterAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func alertmanagerClusterAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
for _, am := range alert.Alertmanager {
|
||||
for _, operator := range operators {
|
||||
switch operator {
|
||||
case equalOperator, notEqualOperator:
|
||||
token := name + operator + am.Cluster
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -65,22 +65,14 @@ func newInhibitedFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func inhibitedAutocomplete(name string, _ []string, _ []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
|
||||
func inhibitedAutocomplete(name string, _ []string, _ []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, val := range []string{trueValue, falseValue} {
|
||||
token := name + equalOperator + val
|
||||
hint := makeAC(token, []string{
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + equalOperator,
|
||||
val,
|
||||
})
|
||||
tokens[token] = &hint
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -63,29 +63,20 @@ func newInhibitedByFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func inhibitedByAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func inhibitedByAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
for _, am := range alert.Alertmanager {
|
||||
for _, silenceID := range am.InhibitedBy {
|
||||
for _, operator := range operators {
|
||||
token := name + operator + silenceID
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silenceID,
|
||||
})
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silenceID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
@@ -10,6 +9,22 @@ import (
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
)
|
||||
|
||||
func isDigits(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for i := range len(s) {
|
||||
c := s[i]
|
||||
if c < '0' || c > '9' {
|
||||
if i == 0 && (c == '+' || c == '-') && len(s) > 1 {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type labelFilter struct {
|
||||
value string
|
||||
alertFilter
|
||||
@@ -46,68 +61,59 @@ func newLabelFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func LabelAutocomplete(labelPairs [][]labels.Label) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func LabelAutocomplete(labelPairs [][]labels.Label, dst map[string]models.Autocomplete) {
|
||||
var b strings.Builder
|
||||
for _, pairs := range labelPairs {
|
||||
for _, l := range pairs {
|
||||
for _, operator := range labelFilterOperators {
|
||||
switch operator {
|
||||
case equalOperator, notEqualOperator:
|
||||
token := l.Name + operator + l.Value
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
b.Reset()
|
||||
b.Grow(len(l.Name) + len(operator) + len(l.Value))
|
||||
b.WriteString(l.Name)
|
||||
b.WriteString(operator)
|
||||
b.WriteString(l.Value)
|
||||
token := b.String()
|
||||
setAC(dst, token, []string{
|
||||
l.Name,
|
||||
l.Name + operator,
|
||||
l.Value,
|
||||
})
|
||||
case regexpOperator, negativeRegexOperator:
|
||||
if strings.Contains(l.Value, " ") {
|
||||
for substring := range strings.SplitSeq(l.Value, " ") {
|
||||
b.Reset()
|
||||
b.Grow(len(l.Name) + len(operator) + len(substring))
|
||||
b.WriteString(l.Name)
|
||||
b.WriteString(operator)
|
||||
b.WriteString(substring)
|
||||
token := b.String()
|
||||
setAC(dst, token, []string{
|
||||
l.Name,
|
||||
l.Name + operator,
|
||||
l.Value,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
case regexpOperator, negativeRegexOperator:
|
||||
substrings := strings.Split(l.Value, " ")
|
||||
if len(substrings) > 1 {
|
||||
for _, substring := range substrings {
|
||||
token := l.Name + operator + substring
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
l.Name,
|
||||
l.Name + operator,
|
||||
l.Value,
|
||||
substring,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
substring,
|
||||
})
|
||||
}
|
||||
}
|
||||
case moreThanOperator, lessThanOperator:
|
||||
if _, err := strconv.Atoi(l.Value); err == nil {
|
||||
token := l.Name + operator + l.Value
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
l.Name,
|
||||
l.Name + operator,
|
||||
l.Value,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
if isDigits(l.Value) {
|
||||
b.Reset()
|
||||
b.Grow(len(l.Name) + len(operator) + len(l.Value))
|
||||
b.WriteString(l.Name)
|
||||
b.WriteString(operator)
|
||||
b.WriteString(l.Value)
|
||||
token := b.String()
|
||||
setAC(dst, token, []string{
|
||||
l.Name,
|
||||
l.Name + operator,
|
||||
l.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
var labelFilterOperators = []string{regexpOperator, negativeRegexOperator, equalOperator, notEqualOperator, lessThanOperator, moreThanOperator}
|
||||
|
||||
@@ -51,25 +51,17 @@ func newLimitFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func limitAutocomplete(name string, operators []string, _ []models.Alert) []models.Autocomplete {
|
||||
tokens := make([]models.Autocomplete, 0, len(operators)*2)
|
||||
func limitAutocomplete(name string, operators []string, _ []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, operator := range operators {
|
||||
tokens = append(tokens, makeAC(
|
||||
fmt.Sprintf("%s%s10", name, operator),
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
))
|
||||
tokens = append(tokens, makeAC(
|
||||
fmt.Sprintf("%s%s50", name, operator),
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
))
|
||||
setAC(dst, fmt.Sprintf("%s%s10", name, operator), []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
setAC(dst, fmt.Sprintf("%s%s50", name, operator), []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
@@ -43,51 +43,32 @@ func newreceiverFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func receiverAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func receiverAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
if alert.Receiver != "" {
|
||||
for _, operator := range operators {
|
||||
switch operator {
|
||||
case equalOperator, notEqualOperator:
|
||||
token := name + operator + alert.Receiver
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
case regexpOperator, negativeRegexOperator:
|
||||
if strings.Contains(alert.Receiver, " ") {
|
||||
for substring := range strings.SplitSeq(alert.Receiver, " ") {
|
||||
token := name + operator + substring
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
case regexpOperator, negativeRegexOperator:
|
||||
substrings := strings.Split(alert.Receiver, " ")
|
||||
if len(substrings) > 1 {
|
||||
for _, substring := range substrings {
|
||||
token := name + operator + substring
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
substring,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
substring,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ func newSilenceAuthorFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func silenceAuthorAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func silenceAuthorAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
for _, am := range alert.Alertmanager {
|
||||
for _, silenceID := range am.SilencedBy {
|
||||
@@ -79,24 +78,16 @@ func silenceAuthorAutocomplete(name string, operators []string, alerts []models.
|
||||
if found {
|
||||
for _, operator := range operators {
|
||||
token := name + operator + silence.CreatedBy
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silence.CreatedBy,
|
||||
})
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silence.CreatedBy,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ func newSilenceTicketFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func silenceTicketIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func silenceTicketIDAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
for _, am := range alert.Alertmanager {
|
||||
for _, silenceID := range am.SilencedBy {
|
||||
@@ -79,24 +78,16 @@ func silenceTicketIDAutocomplete(name string, operators []string, alerts []model
|
||||
if found && silence.TicketID != "" {
|
||||
for _, operator := range operators {
|
||||
token := name + operator + silence.TicketID
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silence.TicketID,
|
||||
})
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silence.TicketID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -63,29 +63,20 @@ func newsilenceIDFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func silenceIDAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func silenceIDAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, alert := range alerts {
|
||||
for _, am := range alert.Alertmanager {
|
||||
for _, silenceID := range am.SilencedBy {
|
||||
for _, operator := range operators {
|
||||
token := name + operator + silenceID
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silenceID,
|
||||
})
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
silenceID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -56,27 +56,15 @@ func newStateFilter() FilterT {
|
||||
return &f
|
||||
}
|
||||
|
||||
func stateAutocomplete(name string, operators []string, alerts []models.Alert) []models.Autocomplete {
|
||||
tokens := map[string]*models.Autocomplete{}
|
||||
func stateAutocomplete(name string, operators []string, alerts []models.Alert, dst map[string]models.Autocomplete) {
|
||||
for _, operator := range operators {
|
||||
for _, alert := range alerts {
|
||||
token := name + operator + alert.State.String()
|
||||
if _, ok := tokens[token]; !ok {
|
||||
hint := makeAC(
|
||||
token,
|
||||
[]string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
},
|
||||
)
|
||||
tokens[token] = &hint
|
||||
}
|
||||
setAC(dst, token, []string{
|
||||
name,
|
||||
strings.TrimPrefix(name, "@"),
|
||||
name + operator,
|
||||
})
|
||||
}
|
||||
}
|
||||
acData := make([]models.Autocomplete, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
acData = append(acData, *token)
|
||||
}
|
||||
return acData
|
||||
}
|
||||
|
||||
@@ -184,6 +184,42 @@ func TestNegativeRegexpMatcherWithInvalidPattern(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDigits(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
// empty string returns false
|
||||
{input: "", expected: false},
|
||||
// plain digits
|
||||
{input: "0", expected: true},
|
||||
{input: "123", expected: true},
|
||||
// positive sign prefix
|
||||
{input: "+1", expected: true},
|
||||
{input: "+999", expected: true},
|
||||
// negative sign prefix
|
||||
{input: "-1", expected: true},
|
||||
{input: "-42", expected: true},
|
||||
// bare sign with no digits returns false
|
||||
{input: "+", expected: false},
|
||||
{input: "-", expected: false},
|
||||
// non-digit characters return false
|
||||
{input: "abc", expected: false},
|
||||
{input: "12a", expected: false},
|
||||
{input: "1.5", expected: false},
|
||||
// sign in the middle returns false
|
||||
{input: "1+2", expected: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := isDigits(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("isDigits(%q) = %v, want %v", tt.input, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMatcher(t *testing.T) {
|
||||
operators := []string{
|
||||
equalOperator,
|
||||
|
||||
@@ -132,6 +132,18 @@ func TestSortOrderedLabels(t *testing.T) {
|
||||
{Name: "bar", Value: "1"},
|
||||
},
|
||||
},
|
||||
// verifies that two distinct labels both in config order sort by their position in the order list
|
||||
{
|
||||
order: []string{"bar", "foo"},
|
||||
in: models.OrderedLabels{
|
||||
{Name: "foo", Value: "1"},
|
||||
{Name: "bar", Value: "1"},
|
||||
},
|
||||
out: models.OrderedLabels{
|
||||
{Name: "bar", Value: "1"},
|
||||
{Name: "foo", Value: "1"},
|
||||
},
|
||||
},
|
||||
// verifies that identical labels stay in their original positions
|
||||
{
|
||||
order: []string{},
|
||||
@@ -186,6 +198,17 @@ func TestSortOrderedLabels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareOrderedLabelsIdentical(t *testing.T) {
|
||||
// verifies that comparing two identical labels returns 0
|
||||
config.Config.Labels.Order = []string{}
|
||||
a := models.OrderedLabel{Name: "foo", Value: "bar"}
|
||||
b := models.OrderedLabel{Name: "foo", Value: "bar"}
|
||||
got := models.CompareOrderedLabels(a, b)
|
||||
if got != 0 {
|
||||
t.Errorf("CompareOrderedLabels(%v, %v) = %d, want 0", a, b, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelsMap(t *testing.T) {
|
||||
type testCaseT struct {
|
||||
labels labels.Labels
|
||||
@@ -268,6 +291,70 @@ func TestAlertStateUnmarshalJSONError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertStateStringUnknown(t *testing.T) {
|
||||
// verifies that an out-of-range AlertState falls back to "unprocessed"
|
||||
unknown := models.AlertState(99)
|
||||
if unknown.String() != "unprocessed" {
|
||||
t.Errorf("AlertState(99).String() = %q, want %q", unknown.String(), "unprocessed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAlertStateUnknown(t *testing.T) {
|
||||
// verifies that parsing an unknown string returns AlertStateUnprocessed
|
||||
got := models.ParseAlertState("bogus")
|
||||
if got != models.AlertStateUnprocessed {
|
||||
t.Errorf("ParseAlertState(%q) = %v, want %v", "bogus", got, models.AlertStateUnprocessed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertStateMarshalText(t *testing.T) {
|
||||
// verifies that MarshalText produces the expected string representation
|
||||
tests := []struct {
|
||||
state models.AlertState
|
||||
expected string
|
||||
}{
|
||||
{state: models.AlertStateUnprocessed, expected: "unprocessed"},
|
||||
{state: models.AlertStateActive, expected: "active"},
|
||||
{state: models.AlertStateSuppressed, expected: "suppressed"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
data, err := tt.state.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalText() error: %v", err)
|
||||
}
|
||||
if string(data) != tt.expected {
|
||||
t.Errorf("MarshalText() = %q, want %q", string(data), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertStateUnmarshalText(t *testing.T) {
|
||||
// verifies that UnmarshalText correctly parses known and unknown state strings
|
||||
tests := []struct {
|
||||
input string
|
||||
expected models.AlertState
|
||||
}{
|
||||
{input: "active", expected: models.AlertStateActive},
|
||||
{input: "suppressed", expected: models.AlertStateSuppressed},
|
||||
{input: "unprocessed", expected: models.AlertStateUnprocessed},
|
||||
{input: "unknown", expected: models.AlertStateUnprocessed},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
var s models.AlertState
|
||||
err := s.UnmarshalText([]byte(tt.input))
|
||||
if err != nil {
|
||||
t.Fatalf("UnmarshalText(%q) error: %v", tt.input, err)
|
||||
}
|
||||
if s != tt.expected {
|
||||
t.Errorf("UnmarshalText(%q) = %v, want %v", tt.input, s, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFingerprints(t *testing.T) {
|
||||
// verifies that UpdateFingerprints produces stable, non-empty fingerprints
|
||||
// including the alertmanager instance, silenced-by, and inhibited-by branches
|
||||
|
||||
@@ -257,6 +257,23 @@ func TestAnnotationsCustomOrderSort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotationsSortBothInOrder(t *testing.T) {
|
||||
// verifies that when both annotations appear in the configured order list,
|
||||
// they sort strictly by their position in that list regardless of alphabetical order
|
||||
config.Config.Annotations.Order = []string{"zzz", "aaa"}
|
||||
annotations := models.Annotations{
|
||||
{Name: "aaa", Value: "v1", Visible: true},
|
||||
{Name: "zzz", Value: "v2", Visible: true},
|
||||
}
|
||||
models.SortAnnotations(annotations)
|
||||
if annotations[0].Name != "zzz" {
|
||||
t.Errorf("Expected 'zzz' (order index 0) to be first, got %q", annotations[0].Name)
|
||||
}
|
||||
if annotations[1].Name != "aaa" {
|
||||
t.Errorf("Expected 'aaa' (order index 1) to be second, got %q", annotations[1].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotationsSortIdenticalNames(t *testing.T) {
|
||||
// verifies that annotations with the same name are treated as equal by the comparator
|
||||
config.Config.Annotations.Order = []string{}
|
||||
|
||||
@@ -430,6 +430,63 @@ func TestMarshalJSONTo_MatchesV1(t *testing.T) {
|
||||
TotalGroups: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
// API grid with populated alert groups containing actual alerts,
|
||||
// exercising the inner alert marshaling loop in APIGrid.MarshalJSONTo
|
||||
name: "APIGrid/with_alerts",
|
||||
val: models.APIGrid{
|
||||
StateCount: map[string]int{"active": 1},
|
||||
LabelName: "cluster",
|
||||
LabelValue: "prod",
|
||||
AlertGroups: []models.APIAlertGroup{
|
||||
{
|
||||
AllLabels: map[string]map[string][]string{
|
||||
"active": {"alertname": {"Test"}},
|
||||
},
|
||||
AlertmanagerCount: map[string]int{"am1": 1},
|
||||
StateCount: map[string]int{"active": 1},
|
||||
Receiver: "default",
|
||||
ID: "group-1",
|
||||
Shared: models.APIAlertGroupSharedMaps{
|
||||
Annotations: models.Annotations{},
|
||||
Labels: models.OrderedLabels{{Name: "job", Value: "test"}},
|
||||
Silences: map[string][]string{},
|
||||
Sources: []string{"http://am1:9093"},
|
||||
Clusters: []string{"cluster1"},
|
||||
},
|
||||
Labels: models.OrderedLabels{{Name: "alertname", Value: "Test"}},
|
||||
Alerts: []models.APIAlert{
|
||||
{
|
||||
StartsAt: ts,
|
||||
State: "active",
|
||||
Receiver: "default",
|
||||
LabelsFP: "fp1",
|
||||
Annotations: models.Annotations{
|
||||
{Name: "summary", Value: "test", Visible: true},
|
||||
},
|
||||
Labels: models.OrderedLabels{
|
||||
{Name: "instance", Value: "localhost:9090"},
|
||||
},
|
||||
Alertmanager: []models.AlertmanagerInstance{
|
||||
{
|
||||
StartsAt: ts,
|
||||
Fingerprint: "fp1",
|
||||
Name: "am1",
|
||||
Cluster: "cluster1",
|
||||
Source: "http://am1:9093",
|
||||
SilencedBy: []string{},
|
||||
InhibitedBy: []string{},
|
||||
State: models.AlertStateActive,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalAlerts: 1,
|
||||
},
|
||||
},
|
||||
TotalGroups: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
// full alert group with all nested structures populated
|
||||
name: "APIAlertGroup/full",
|
||||
|
||||
Reference in New Issue
Block a user