Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add grouping verification to routing test command #4208

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0bc0c15
Release v0.28.0
SuperQ Dec 19, 2024
c21f7af
chore(deps): bump `github.com/prometheus/common` from 0.60.0 to 0.61.…
mmorel-35 Dec 5, 2024
e6e2283
feat(cli): add grouping verification to routing test command
heartwilltell Jan 16, 2025
6967cff
Rename groupingStr to groupingSlug
heartwilltell Jan 16, 2025
5c21da0
feat(cli): enhance routing test command with receiver grouping support
heartwilltell Jan 16, 2025
ca1b190
refactor(cli): improve comments in routingShow struct
heartwilltell Jan 16, 2025
41ef883
syntax fixes
heartwilltell Jan 16, 2025
988ee8b
feat(cli): enhance receiver parsing to support nested groupings
heartwilltell Jan 17, 2025
cc94e98
fix(cli): improve receiver parsing by trimming spaces and enhancing b…
heartwilltell Jan 20, 2025
eb735b4
Syntax fixes.
heartwilltell Jan 20, 2025
9eaeae8
Syntax fixes.
heartwilltell Jan 20, 2025
7fb1e0d
refactor(cli): enhance receiver parsing and validation
heartwilltell Jan 20, 2025
2fd5775
refactor(cli): streamline receiver parsing logic in `parseReceiversWi…
heartwilltell Jan 20, 2025
ff493d4
refactor(cli): optimize receiver grouping verification in `verifyRece…
heartwilltell Jan 20, 2025
ebd7b55
refactor(cli): improve error messaging in `parseLabelSet` and `verify…
heartwilltell Jan 20, 2025
6f1a243
refactor(cli): enhance grouping verification in `verifyReceiversGroup…
heartwilltell Jan 20, 2025
ebd1492
feat(cli): improve receiver parsing with quote and bracket handling
heartwilltell Jan 28, 2025
061d593
refactor(cli): remove unused `removeSpacesAroundCommas` function
heartwilltell Jan 28, 2025
77c7531
Merge branch 'main' into alert-grouping-test
heartwilltell Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(cli): enhance receiver parsing and validation
- Introduced `removeSpacesAroundCommas` function to streamline input processing by removing unnecessary spaces around commas.
- Improved `parseReceiversWithGrouping` to better handle nested groupings and enhance error messaging for invalid formats.
- Added `sortGroupLabels` function to return sorted group labels for improved consistency in output.
- Updated `verifyReceivers` and `verifyReceiversGrouping` functions to provide clearer error messages when expected and actual receivers do not match.
- Enhanced `formatOutput` to generate a more structured output of receivers and their groupings.

These changes improve the robustness and usability of the receiver parsing and validation functionality in the routing test command.

Signed-off-by: heartwilltell <[email protected]>
  • Loading branch information
heartwilltell committed Jan 28, 2025
commit 7fb1e0d8fbf55d2eece2c658344dc6f8b63a18d0
257 changes: 152 additions & 105 deletions cli/test_routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import (
"context"
"fmt"
"os"
"slices"
"strings"

"github.com/alecthomas/kingpin/v2"
"github.com/prometheus/common/model"
"github.com/xlab/treeprint"

"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/dispatch"
"github.com/prometheus/alertmanager/matcher/compat"
"github.com/prometheus/alertmanager/pkg/labels"
)

const routingTestHelp = `Test alert routing
Expand Down Expand Up @@ -77,35 +78,42 @@ func printMatchingTree(mainRoute *dispatch.Route, ls models.LabelSet) {
func parseReceiversWithGrouping(input string) (map[string][]string, error) {
result := make(map[string][][]string) // maps receiver to list of possible groupings.

// Remove spaces around commas.
input = strings.ReplaceAll(input, " ,", ",")
input = strings.ReplaceAll(input, ", ", ",")
input = removeSpacesAroundCommas(input)

// If no square brackets in input, treat it as simple receiver list
// If no square brackets in input, treat it as simple receiver list.
if !strings.Contains(input, "[") {
receivers := strings.Split(input, ",")

for _, r := range receivers {
r = strings.TrimSpace(r)
if r != "" {
result[r] = nil
}
}

return flattenGroupingMap(result), nil
}

// Split by comma but preserve commas within square brackets
var receivers []string
var currentReceiver strings.Builder
// Split by comma but preserve commas within square brackets.
var (
receivers []string
currentReceiver strings.Builder
)

inBrackets := false
bracketCount := 0

for i := 0; i < len(input); i++ {
char := input[i]

if char == '[' {
inBrackets = true
bracketCount++
} else if char == ']' {
}

if char == ']' {
bracketCount--

if bracketCount == 0 {
inBrackets = false
}
Expand All @@ -120,6 +128,7 @@ func parseReceiversWithGrouping(input string) (map[string][]string, error) {
currentReceiver.WriteByte(char)
}
}

if currentReceiver.Len() > 0 {
receivers = append(receivers, strings.TrimSpace(currentReceiver.String()))
}
Expand All @@ -132,7 +141,7 @@ func parseReceiversWithGrouping(input string) (map[string][]string, error) {

bracketIndex := strings.LastIndex(r, "[")
if bracketIndex == -1 {
// No grouping specified
// No grouping specified.
result[r] = nil
continue
}
Expand Down Expand Up @@ -162,11 +171,21 @@ func parseReceiversWithGrouping(input string) (map[string][]string, error) {
if result[receiverName] == nil {
result[receiverName] = make([][]string, 0)
}

result[receiverName] = append(result[receiverName], cleanGroups)
}

return flattenGroupingMap(result), nil
}

// removeSpacesAroundCommas removes spaces around commas.
func removeSpacesAroundCommas(input string) string {
input = strings.ReplaceAll(input, " ,", ",")
input = strings.ReplaceAll(input, ", ", ",")

return input
}

// flattenGroupingMap converts the internal map[string][][]string to the expected map[string][]string format.
func flattenGroupingMap(input map[string][][]string) map[string][]string {
result := make(map[string][]string)
Expand All @@ -190,144 +209,172 @@ func flattenGroupingMap(input map[string][][]string) map[string][]string {
return result
}

func (c *routingShow) routingTestAction(ctx context.Context, _ *kingpin.ParseContext) error {
cfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile)
if err != nil {
kingpin.Fatalf("%v\n", err)
return err
}
// sortGroupLabels returns a sorted slice of group labels.
func sortGroupLabels(groupBy map[model.LabelName]struct{}) []string {
result := make([]string, 0, len(groupBy))

if c.expectedReceiversGroup != "" {
c.receiversGrouping, err = parseReceiversWithGrouping(c.expectedReceiversGroup)
if err != nil {
kingpin.Fatalf("Failed to parse receivers with grouping: %v\n", err)
}
for k := range groupBy {
result = append(result, string(k))
}

mainRoute := dispatch.NewRoute(cfg.Route, nil)
slices.Sort[[]string](result)

return result
}

// Parse labels to LabelSet.
ls := make(models.LabelSet, len(c.labels))
for _, l := range c.labels {
// parseLabelSet parses command line labels into a LabelSet.
func parseLabelSet(labels []string) (models.LabelSet, error) {
ls := make(models.LabelSet, len(labels))
for _, l := range labels {
matcher, err := compat.Matcher(l, "cli")
if err != nil {
kingpin.Fatalf("Failed to parse labels: %v\n", err)
return nil, fmt.Errorf("failed to parse labels: %w", err)
}
if matcher.Type != labels.MatchEqual {
kingpin.Fatalf("%s\n", "Labels must be specified as key=value pairs")

if matcher.Type != 0 { // 0 is labels.MatchEqual
return nil, fmt.Errorf("labels must be specified as key=value pairs")
}

ls[matcher.Name] = matcher.Value
}
return ls, nil
}

if c.debugTree {
printMatchingTree(mainRoute, ls)
// verifyReceivers checks if the actual receivers match the expected ones.
func verifyReceivers(expected, actual string) error {
if expected == "" {
return nil
}
expectedReceivers := strings.Split(expected, ",")
actualReceivers := strings.Split(actual, ",")

receivers, err := resolveAlertReceivers(mainRoute, &ls)
if err != nil {
return err
if !slices.Equal[[]string](expectedReceivers, actualReceivers) {
return fmt.Errorf("expected receivers did not match resolved receivers.\nExpected: %v\nGot: %v",
expectedReceivers, actualReceivers)
}

receiversSlug := strings.Join(receivers, ",")
finalRoutes := mainRoute.Match(convertClientToCommonLabelSet(ls))

// Verify receivers.
if c.expectedReceivers != "" {
expectedReceivers := strings.Split(c.expectedReceivers, ",")
actualReceivers := strings.Split(receiversSlug, ",")
return nil
}

if !stringSlicesEqual(expectedReceivers, actualReceivers) {
fmt.Printf("WARNING: Expected receivers did not match resolved receivers.\nExpected: %v\nGot: %v\n",
expectedReceivers, actualReceivers)
os.Exit(1)
}
// verifyReceiversGrouping checks if receivers and their groupings match the expected configuration.
func verifyReceiversGrouping(receiversGrouping map[string][]string, finalRoutes []*dispatch.Route) error {
if len(receiversGrouping) == 0 {
return nil
}

// Verify receivers and their grouping.
if len(c.receiversGrouping) > 0 {
matchedReceivers := make(map[string]bool)
matchedReceivers := make(map[string]bool)

for _, route := range finalRoutes {
receiver := route.RouteOpts.Receiver
actualGroups := make([]string, 0, len(route.RouteOpts.GroupBy))
for _, route := range finalRoutes {
receiver := route.RouteOpts.Receiver
actualGroups := sortGroupLabels(route.RouteOpts.GroupBy)

for k := range route.RouteOpts.GroupBy {
actualGroups = append(actualGroups, string(k))
}
// Try to match with any of the expected groupings.
matched := false

// Try to match with any of the expected groupings.
matched := false
for expectedReceiver, expectedGroups := range receiversGrouping {
baseReceiver := strings.Split(expectedReceiver, "_")[0]

for expectedReceiver, expectedGroups := range c.receiversGrouping {
baseReceiver := strings.Split(expectedReceiver, "_")[0]
if baseReceiver == receiver && expectedGroups != nil {
if slices.Equal[[]string](expectedGroups, actualGroups) {
matchedReceivers[expectedReceiver] = true
matched = true

if baseReceiver == receiver && expectedGroups != nil {
if stringSlicesEqual(expectedGroups, actualGroups) {
matchedReceivers[expectedReceiver] = true
matched = true
break
}
break
}
}

if !matched && c.receiversGrouping[receiver] != nil {
fmt.Printf("WARNING: No matching grouping found for receiver %s with groups %v\n",
receiver, actualGroups)
os.Exit(1)
}
}

// Check if all expected receivers with grouping were matched.
for expectedReceiver, expectedGroups := range c.receiversGrouping {
if expectedGroups != nil && !matchedReceivers[expectedReceiver] {
fmt.Printf("WARNING: Expected receiver %s with grouping %v was not matched\n",
expectedReceiver, expectedGroups)
os.Exit(1)
}
if !matched && receiversGrouping[receiver] != nil {
return fmt.Errorf("no matching grouping found for receiver %s with groups [%s]",
receiver,
strings.Join(actualGroups, ","),
)
}
}

var output strings.Builder
output.WriteString(receiversSlug)
// Check if all expected receivers with grouping were matched.
for expectedReceiver, expectedGroups := range receiversGrouping {
if expectedGroups != nil && !matchedReceivers[expectedReceiver] {
slices.Sort[[]string](expectedGroups)

if len(finalRoutes) > 0 {
for _, route := range finalRoutes {
if len(route.RouteOpts.GroupBy) > 0 {
groupBySlice := make([]string, 0, len(route.RouteOpts.GroupBy))
for k := range route.RouteOpts.GroupBy {
groupBySlice = append(groupBySlice, string(k))
}
output.WriteString(fmt.Sprintf("[%s]", strings.Join(groupBySlice, ",")))
}
return fmt.Errorf("expected receiver %s with grouping [%s] was not matched",
expectedReceiver,
strings.Join(expectedGroups, ","),
)
}
}

fmt.Println(output.String())
return nil
}

func stringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
// formatOutput generates the output string showing receivers and their groupings.
func formatOutput(finalRoutes []*dispatch.Route) string {
var (
sb strings.Builder
first = true
)

// Create maps to count occurrences
mapA := make(map[string]int)
mapB := make(map[string]int)
for _, route := range finalRoutes {
if !first {
sb.WriteString(",")
}

first = false
sb.WriteString(route.RouteOpts.Receiver)

for _, val := range a {
mapA[val]++
if len(route.RouteOpts.GroupBy) > 0 {
groupBySlice := sortGroupLabels(route.RouteOpts.GroupBy)
sb.WriteString(fmt.Sprintf("[%s]", strings.Join(groupBySlice, ",")))
}
}
for _, val := range b {
mapB[val]++

return sb.String()
}

func (c *routingShow) routingTestAction(ctx context.Context, _ *kingpin.ParseContext) error {
cfg, err := loadAlertmanagerConfig(ctx, alertmanagerURL, c.configFile)
if err != nil {
kingpin.Fatalf("%v\n", err)
return err
}

// Compare maps
for key, countA := range mapA {
if countB, exists := mapB[key]; !exists || countA != countB {
return false
if c.expectedReceiversGroup != "" {
c.receiversGrouping, err = parseReceiversWithGrouping(c.expectedReceiversGroup)
if err != nil {
kingpin.Fatalf("Failed to parse receivers with grouping: %v\n", err)
}
}

return true
mainRoute := dispatch.NewRoute(cfg.Route, nil)

ls, err := parseLabelSet(c.labels)
if err != nil {
kingpin.Fatalf("%v\n", err)
}

if c.debugTree {
printMatchingTree(mainRoute, ls)
}

receivers, err := resolveAlertReceivers(mainRoute, &ls)
if err != nil {
return err
}

receiversSlug := strings.Join(receivers, ",")
finalRoutes := mainRoute.Match(convertClientToCommonLabelSet(ls))

if err := verifyReceivers(c.expectedReceivers, receiversSlug); err != nil {
fmt.Printf("WARNING: %v\n", err)
os.Exit(1)
}

if err := verifyReceiversGrouping(c.receiversGrouping, finalRoutes); err != nil {
fmt.Printf("WARNING: %v\n", err)
os.Exit(1)
}

fmt.Println(formatOutput(finalRoutes))

return nil
}