Skip to content

Commit

Permalink
Support UTF-8 label matchers: Add compat package with feature flag an…
Browse files Browse the repository at this point in the history
…d use in amtool (#3483)

* Add adapter package for parser feature flag

This commit adds the compat package allowing users to switch
between the new matchers/parse parser and the old pkg/labels parser.
The new matchers/parse parser uses a fallback mechanism where if
the input cannot be parsed in the new parser it then attempts to
use the old parser. If an input is parsed in the old parser but
not the new parser, then a warning log is emitted.

---------

Signed-off-by: George Robinson <george.robinson@grafana.com>
  • Loading branch information
grobinson-grafana committed Oct 19, 2023
1 parent 412f062 commit 16aa996
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 65 deletions.
40 changes: 30 additions & 10 deletions cli/alert_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ package cli

import (
"context"
"errors"
"fmt"
"strconv"
"time"

"github.com/alecthomas/kingpin/v2"
"github.com/go-openapi/strfmt"

"github.com/prometheus/alertmanager/api/v2/client/alert"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/matchers/compat"
"github.com/prometheus/alertmanager/pkg/labels"
)

type alertAddCmd struct {
Expand Down Expand Up @@ -73,29 +77,45 @@ func (a *alertAddCmd) addAlert(ctx context.Context, _ *kingpin.ParseContext) err
if len(a.labels) > 0 {
// Allow the alertname label to be defined implicitly as the first argument rather
// than explicitly as a key=value pair.
if _, err := parseLabels([]string{a.labels[0]}); err != nil {
a.labels[0] = fmt.Sprintf("alertname=%s", a.labels[0])
if _, err := compat.Matcher(a.labels[0]); err != nil {
a.labels[0] = fmt.Sprintf("alertname=%s", strconv.Quote(a.labels[0]))
}
}

labels, err := parseLabels(a.labels)
if err != nil {
return err
ls := make(models.LabelSet, len(a.labels))
for _, l := range a.labels {
matcher, err := compat.Matcher(l)
if err != nil {
return err
}
if matcher.Type != labels.MatchEqual {
return errors.New("labels must be specified as key=value pairs")
}
ls[matcher.Name] = matcher.Value
}

annotations, err := parseLabels(a.annotations)
if err != nil {
return err
annotations := make(models.LabelSet, len(a.annotations))
for _, a := range a.annotations {
matcher, err := compat.Matcher(a)
if err != nil {
return err
}
if matcher.Type != labels.MatchEqual {
return errors.New("annotations must be specified as key=value pairs")
}
annotations[matcher.Name] = matcher.Value
}

var startsAt, endsAt time.Time
if a.start != "" {
var err error
startsAt, err = time.Parse(time.RFC3339, a.start)
if err != nil {
return err
}
}
if a.end != "" {
var err error
endsAt, err = time.Parse(time.RFC3339, a.end)
if err != nil {
return err
Expand All @@ -105,7 +125,7 @@ func (a *alertAddCmd) addAlert(ctx context.Context, _ *kingpin.ParseContext) err
pa := &models.PostableAlert{
Alert: models.Alert{
GeneratorURL: strfmt.URI(a.generatorURL),
Labels: labels,
Labels: ls,
},
Annotations: annotations,
StartsAt: strfmt.DateTime(startsAt),
Expand All @@ -116,6 +136,6 @@ func (a *alertAddCmd) addAlert(ctx context.Context, _ *kingpin.ParseContext) err

amclient := NewAlertmanagerClient(alertmanagerURL)

_, err = amclient.Alert.PostAlerts(alertParams)
_, err := amclient.Alert.PostAlerts(alertParams)
return err
}
4 changes: 2 additions & 2 deletions cli/alert_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (

"github.com/prometheus/alertmanager/api/v2/client/alert"
"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/alertmanager/matchers/compat"
)

type alertQueryCmd struct {
Expand Down Expand Up @@ -80,7 +80,7 @@ func (a *alertQueryCmd) queryAlerts(ctx context.Context, _ *kingpin.ParseContext
// the user wants alertname=<arg> and prepend `alertname=` to
// the front.
m := a.matcherGroups[0]
_, err := labels.ParseMatcher(m)
_, err := compat.Matcher(m)
if err != nil {
a.matcherGroups[0] = fmt.Sprintf("alertname=%s", m)
}
Expand Down
27 changes: 25 additions & 2 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ import (
"net/url"
"os"
"path"
"strings"
"time"

"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
clientruntime "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
promconfig "github.com/prometheus/common/config"
"github.com/prometheus/common/version"
Expand All @@ -29,8 +33,8 @@ import (
"github.com/prometheus/alertmanager/api/v2/client"
"github.com/prometheus/alertmanager/cli/config"
"github.com/prometheus/alertmanager/cli/format"

clientruntime "github.com/go-openapi/runtime/client"
"github.com/prometheus/alertmanager/featurecontrol"
"github.com/prometheus/alertmanager/matchers/compat"
)

var (
Expand All @@ -40,11 +44,27 @@ var (
timeout time.Duration
httpConfigFile string
versionCheck bool
featureFlags string

configFiles = []string{os.ExpandEnv("$HOME/.config/amtool/config.yml"), "/etc/amtool/config.yml"}
legacyFlags = map[string]string{"comment_required": "require-comment"}
)

func initMatchersCompat(_ *kingpin.ParseContext) error {
logger := log.NewLogfmtLogger(os.Stdout)
if verbose {
logger = level.NewFilter(logger, level.AllowDebug())
} else {
logger = level.NewFilter(logger, level.AllowInfo())
}
featureConfig, err := featurecontrol.NewFlags(logger, featureFlags)
if err != nil {
kingpin.Fatalf("error parsing the feature flag list: %v\n", err)
}
compat.InitFromFlags(logger, featureConfig)
return nil
}

func requireAlertManagerURL(pc *kingpin.ParseContext) error {
// Return without error if any help flag is set.
for _, elem := range pc.Elements {
Expand Down Expand Up @@ -137,6 +157,7 @@ func Execute() {
app.Flag("timeout", "Timeout for the executed command").Default("30s").DurationVar(&timeout)
app.Flag("http.config.file", "HTTP client configuration file for amtool to connect to Alertmanager.").PlaceHolder("<filename>").ExistingFileVar(&httpConfigFile)
app.Flag("version-check", "Check alertmanager version. Use --no-version-check to disable.").Default("true").BoolVar(&versionCheck)
app.Flag("enable-feature", fmt.Sprintf("Experimental features to enable. The flag can be repeated to enable multiple features. Valid options: %s", strings.Join(featurecontrol.AllowedFlags, ", "))).Default("").StringVar(&featureFlags)

app.Version(version.Print("amtool"))
app.GetFlag("help").Short('h')
Expand All @@ -154,6 +175,8 @@ func Execute() {
configureConfigCmd(app)
configureTemplateCmd(app)

app.Action(initMatchersCompat)

err = resolver.Bind(app, os.Args[1:])
if err != nil {
kingpin.Fatalf("%v\n", err)
Expand Down
18 changes: 12 additions & 6 deletions cli/silence_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"os/user"
"strconv"
"time"

"github.com/alecthomas/kingpin/v2"
Expand All @@ -26,6 +27,8 @@ import (

"github.com/prometheus/alertmanager/api/v2/client/silence"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/matchers/compat"
"github.com/prometheus/alertmanager/pkg/labels"
)

func username() string {
Expand Down Expand Up @@ -92,17 +95,20 @@ func (c *silenceAddCmd) add(ctx context.Context, _ *kingpin.ParseContext) error
// If the parser fails then we likely don't have a (=|=~|!=|!~) so lets
// assume that the user wants alertname=<arg> and prepend `alertname=`
// to the front.
_, err := parseMatchers([]string{c.matchers[0]})
_, err := compat.Matcher(c.matchers[0])
if err != nil {
c.matchers[0] = fmt.Sprintf("alertname=%s", c.matchers[0])
c.matchers[0] = fmt.Sprintf("alertname=%s", strconv.Quote(c.matchers[0]))
}
}

matchers, err := parseMatchers(c.matchers)
if err != nil {
return err
matchers := make([]labels.Matcher, 0, len(c.matchers))
for _, s := range c.matchers {
m, err := compat.Matcher(s)
if err != nil {
return err
}
matchers = append(matchers, *m)
}

if len(matchers) < 1 {
return fmt.Errorf("no matchers specified")
}
Expand Down
7 changes: 4 additions & 3 deletions cli/silence_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import (
"context"
"errors"
"fmt"
"strconv"
"time"

kingpin "github.com/alecthomas/kingpin/v2"

"github.com/prometheus/alertmanager/api/v2/client/silence"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/alertmanager/matchers/compat"
)

type silenceQueryCmd struct {
Expand Down Expand Up @@ -98,9 +99,9 @@ func (c *silenceQueryCmd) query(ctx context.Context, _ *kingpin.ParseContext) er
// If the parser fails then we likely don't have a (=|=~|!=|!~) so lets
// assume that the user wants alertname=<arg> and prepend `alertname=`
// to the front.
_, err := labels.ParseMatcher(c.matchers[0])
_, err := compat.Matcher(c.matchers[0])
if err != nil {
c.matchers[0] = fmt.Sprintf("alertname=%s", c.matchers[0])
c.matchers[0] = fmt.Sprintf("alertname=%s", strconv.Quote(c.matchers[0]))
}
}

Expand Down
15 changes: 12 additions & 3 deletions cli/test_routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (

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

const routingTestHelp = `Test alert routing
Expand Down Expand Up @@ -80,9 +82,16 @@ func (c *routingShow) routingTestAction(ctx context.Context, _ *kingpin.ParseCon
mainRoute := dispatch.NewRoute(cfg.Route, nil)

// Parse labels to LabelSet.
ls, err := parseLabels(c.labels)
if err != nil {
kingpin.Fatalf("Failed to parse labels: %v\n", err)
ls := make(models.LabelSet, len(c.labels))
for _, l := range c.labels {
matcher, err := compat.Matcher(l)
if err != nil {
kingpin.Fatalf("Failed to parse labels: %v\n", err)
}
if matcher.Type != labels.MatchEqual {
kingpin.Fatalf("%s\n", "Labels must be specified as key=value pairs")
}
ls[matcher.Name] = matcher.Value
}

if c.debugTree {
Expand Down
37 changes: 1 addition & 36 deletions cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"net/url"
"os"

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

"github.com/prometheus/alertmanager/api/v2/client/general"
Expand All @@ -29,22 +29,6 @@ import (
"github.com/prometheus/alertmanager/pkg/labels"
)

// parseMatchers parses a list of matchers (cli arguments).
func parseMatchers(inputMatchers []string) ([]labels.Matcher, error) {
matchers := make([]labels.Matcher, 0, len(inputMatchers))

for _, v := range inputMatchers {
matcher, err := labels.ParseMatcher(v)
if err != nil {
return []labels.Matcher{}, err
}

matchers = append(matchers, *matcher)
}

return matchers, nil
}

// getRemoteAlertmanagerConfigStatus returns status responsecontaining configuration from remote Alertmanager
func getRemoteAlertmanagerConfigStatus(ctx context.Context, alertmanagerURL *url.URL) (*models.AlertmanagerStatus, error) {
amclient := NewAlertmanagerClient(alertmanagerURL)
Expand Down Expand Up @@ -94,25 +78,6 @@ func convertClientToCommonLabelSet(cls models.LabelSet) model.LabelSet {
return mls
}

// parseLabels parses a list of labels (cli arguments).
func parseLabels(inputLabels []string) (models.LabelSet, error) {
labelSet := make(models.LabelSet, len(inputLabels))

for _, l := range inputLabels {
matcher, err := labels.ParseMatcher(l)
if err != nil {
return models.LabelSet{}, err
}
if matcher.Type != labels.MatchEqual {
return models.LabelSet{}, errors.New("labels must be specified as key=value pairs")
}

labelSet[matcher.Name] = matcher.Value
}

return labelSet, nil
}

// TypeMatchers only valid for when you are going to add a silence
func TypeMatchers(matchers []labels.Matcher) models.Matchers {
typeMatchers := make(models.Matchers, len(matchers))
Expand Down

0 comments on commit 16aa996

Please sign in to comment.