Add support for adding alerts using amtool (#1461)

* Add support for adding alerts using amtool

Signed-off-by: Bob Shannon <bshannon@palantir.com>

* comment: Simplify return in addAlert

Signed-off-by: Bob Shannon <bshannon@palantir.com>
This commit is contained in:
Bob Shannon 2018-07-16 10:29:04 -04:00 committed by stuart nelson
parent 81cc0ffa12
commit 50e271678d
4 changed files with 256 additions and 96 deletions

View File

@ -14,101 +14,11 @@
package cli
import (
"context"
"errors"
"fmt"
"strings"
"github.com/prometheus/client_golang/api"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/client"
"github.com/prometheus/alertmanager/pkg/parse"
)
type alertQueryCmd struct {
inhibited, silenced, active, unprocessed bool
receiver string
matcherGroups []string
}
const alertHelp = `View and search through current alerts.
Amtool has a simplified prometheus query syntax, but contains robust support for
bash variable expansions. The non-option section of arguments constructs a list
of "Matcher Groups" that will be used to filter your query. The following
examples will attempt to show this behaviour in action:
amtool alert query alertname=foo node=bar
This query will match all alerts with the alertname=foo and node=bar label
value pairs set.
amtool alert query foo node=bar
If alertname is omitted and the first argument does not contain a '=' or a
'=~' then it will be assumed to be the value of the alertname pair.
amtool alert query 'alertname=~foo.*'
As well as direct equality, regex matching is also supported. The '=~' syntax
(similar to prometheus) is used to represent a regex match. Regex matching
can be used in combination with a direct match.
Amtool supports several flags for filtering the returned alerts by state
(inhibited, silenced, active, unprocessed). If none of these flags is given,
only active alerts are returned.
`
func configureAlertCmd(app *kingpin.Application) {
var (
a = &alertQueryCmd{}
alertCmd = app.Command("alert", alertHelp).PreAction(requireAlertManagerURL)
queryCmd = alertCmd.Command("query", alertHelp).Default()
)
queryCmd.Flag("inhibited", "Show inhibited alerts").Short('i').BoolVar(&a.inhibited)
queryCmd.Flag("silenced", "Show silenced alerts").Short('s').BoolVar(&a.silenced)
queryCmd.Flag("active", "Show active alerts").Short('a').BoolVar(&a.active)
queryCmd.Flag("unprocessed", "Show unprocessed alerts").Short('u').BoolVar(&a.unprocessed)
queryCmd.Flag("receiver", "Show alerts matching receiver (Supports regex syntax)").Short('r').StringVar(&a.receiver)
queryCmd.Arg("matcher-groups", "Query filter").StringsVar(&a.matcherGroups)
queryCmd.Action(a.queryAlerts)
}
func (a *alertQueryCmd) queryAlerts(ctx *kingpin.ParseContext) error {
var filterString = ""
if len(a.matcherGroups) == 1 {
// 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 := parse.Matcher(a.matcherGroups[0])
if err != nil {
filterString = fmt.Sprintf("{alertname=%s}", a.matcherGroups[0])
} else {
filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ","))
}
} else if len(a.matcherGroups) > 1 {
filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ","))
}
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
if err != nil {
return err
}
alertAPI := client.NewAlertAPI(c)
// If no selector was passed, default to showing active alerts.
if !a.silenced && !a.inhibited && !a.active && !a.unprocessed {
a.active = true
}
fetchedAlerts, err := alertAPI.List(context.Background(), filterString, a.receiver, a.silenced, a.inhibited, a.active, a.unprocessed)
if err != nil {
return err
}
formatter, found := format.Formatters[output]
if !found {
return errors.New("unknown output formatter")
}
return formatter.FormatAlerts(fetchedAlerts)
alertCmd := app.Command("alert", "Add or query alerts.").PreAction(requireAlertManagerURL)
configureQueryAlertsCmd(alertCmd)
configureAddAlertCmd(alertCmd)
}

116
cli/alert_add.go Normal file
View File

@ -0,0 +1,116 @@
// Copyright 2018 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"fmt"
"github.com/prometheus/alertmanager/client"
"github.com/prometheus/client_golang/api"
"gopkg.in/alecthomas/kingpin.v2"
"time"
)
type alertAddCmd struct {
annotations []string
generatorUrl string
labels []string
start string
end string
}
const alertAddHelp = `Add a new alert.
This command is used to add a new alert to Alertmanager.
To add a new alert with labels:
amtool alert add alertname=foo node=bar
If alertname is omitted and the first argument does not contain a '=' then it will
be assumed to be the value of the alertname pair.
amtool alert add foo node=bar
One or more annotations can be added using the --annotation flag:
amtool alert add foo node=bar \
--annotation=runbook='http://runbook.biz' \
--annotation=summary='summary of the alert' \
--annotation=description='description of the alert'
Additional flags such as --generator-url, --start, and --end are also supported.
`
func configureAddAlertCmd(cc *kingpin.CmdClause) {
var (
a = &alertAddCmd{}
addCmd = cc.Command("add", alertAddHelp)
)
addCmd.Arg("labels", "List of labels to be included with the alert").StringsVar(&a.labels)
addCmd.Flag("generator-url", "Set the URL of the source that generated the alert").StringVar(&a.generatorUrl)
addCmd.Flag("start", "Set when the alert should start. RFC3339 format 2006-01-02T15:04:05Z07:00").StringVar(&a.start)
addCmd.Flag("end", "Set when the alert should should end. RFC3339 format 2006-01-02T15:04:05Z07:00").StringVar(&a.end)
addCmd.Flag("annotation", "Set an annotation to be included with the alert").StringsVar(&a.annotations)
addCmd.Action(a.addAlert)
}
func (a *alertAddCmd) addAlert(ctx *kingpin.ParseContext) error {
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
if err != nil {
return err
}
alertAPI := client.NewAlertAPI(c)
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])
}
}
labels, err := parseLabels(a.labels)
if err != nil {
return err
}
annotations, err := parseLabels(a.annotations)
if err != nil {
return err
}
var startsAt, endsAt time.Time
if a.start != "" {
startsAt, err = time.Parse(time.RFC3339, a.start)
if err != nil {
return err
}
}
if a.end != "" {
endsAt, err = time.Parse(time.RFC3339, a.end)
if err != nil {
return err
}
}
return alertAPI.Push(context.Background(), client.Alert{
Labels: labels,
Annotations: annotations,
StartsAt: startsAt,
EndsAt: endsAt,
GeneratorURL: a.generatorUrl,
})
}

113
cli/alert_query.go Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2018 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"errors"
"fmt"
"strings"
"github.com/prometheus/client_golang/api"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/prometheus/alertmanager/cli/format"
"github.com/prometheus/alertmanager/client"
"github.com/prometheus/alertmanager/pkg/parse"
)
type alertQueryCmd struct {
inhibited, silenced, active, unprocessed bool
receiver string
matcherGroups []string
}
const alertQueryHelp = `View and search through current alerts.
Amtool has a simplified prometheus query syntax, but contains robust support for
bash variable expansions. The non-option section of arguments constructs a list
of "Matcher Groups" that will be used to filter your query. The following
examples will attempt to show this behaviour in action:
amtool alert query alertname=foo node=bar
This query will match all alerts with the alertname=foo and node=bar label
value pairs set.
amtool alert query foo node=bar
If alertname is omitted and the first argument does not contain a '=' or a
'=~' then it will be assumed to be the value of the alertname pair.
amtool alert query 'alertname=~foo.*'
As well as direct equality, regex matching is also supported. The '=~' syntax
(similar to prometheus) is used to represent a regex match. Regex matching
can be used in combination with a direct match.
Amtool supports several flags for filtering the returned alerts by state
(inhibited, silenced, active, unprocessed). If none of these flags is given,
only active alerts are returned.
`
func configureQueryAlertsCmd(cc *kingpin.CmdClause) {
var (
a = &alertQueryCmd{}
queryCmd = cc.Command("query", alertQueryHelp).Default()
)
queryCmd.Flag("inhibited", "Show inhibited alerts").Short('i').BoolVar(&a.inhibited)
queryCmd.Flag("silenced", "Show silenced alerts").Short('s').BoolVar(&a.silenced)
queryCmd.Flag("active", "Show active alerts").Short('a').BoolVar(&a.active)
queryCmd.Flag("unprocessed", "Show unprocessed alerts").Short('u').BoolVar(&a.unprocessed)
queryCmd.Flag("receiver", "Show alerts matching receiver (Supports regex syntax)").Short('r').StringVar(&a.receiver)
queryCmd.Arg("matcher-groups", "Query filter").StringsVar(&a.matcherGroups)
queryCmd.Action(a.queryAlerts)
}
func (a *alertQueryCmd) queryAlerts(ctx *kingpin.ParseContext) error {
var filterString = ""
if len(a.matcherGroups) == 1 {
// 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 := parse.Matcher(a.matcherGroups[0])
if err != nil {
filterString = fmt.Sprintf("{alertname=%s}", a.matcherGroups[0])
} else {
filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ","))
}
} else if len(a.matcherGroups) > 1 {
filterString = fmt.Sprintf("{%s}", strings.Join(a.matcherGroups, ","))
}
c, err := api.NewClient(api.Config{Address: alertmanagerURL.String()})
if err != nil {
return err
}
alertAPI := client.NewAlertAPI(c)
// If no selector was passed, default to showing active alerts.
if !a.silenced && !a.inhibited && !a.active && !a.unprocessed {
a.active = true
}
fetchedAlerts, err := alertAPI.List(context.Background(), filterString, a.receiver, a.silenced, a.inhibited, a.active, a.unprocessed)
if err != nil {
return err
}
formatter, found := format.Formatters[output]
if !found {
return errors.New("unknown output formatter")
}
return formatter.FormatAlerts(fetchedAlerts)
}

View File

@ -14,10 +14,12 @@
package cli
import (
"errors"
"fmt"
"net/url"
"path"
"github.com/prometheus/alertmanager/client"
"github.com/prometheus/alertmanager/pkg/parse"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
@ -45,11 +47,11 @@ func GetAlertmanagerURL(p string) url.URL {
return amURL
}
// Parse a list of labels (cli arguments)
func parseMatchers(inputLabels []string) ([]labels.Matcher, error) {
// Parse a list of matchers (cli arguments)
func parseMatchers(inputMatchers []string) ([]labels.Matcher, error) {
matchers := make([]labels.Matcher, 0)
for _, v := range inputLabels {
for _, v := range inputMatchers {
name, value, matchType, err := parse.Input(v)
if err != nil {
return []labels.Matcher{}, err
@ -65,6 +67,25 @@ func parseMatchers(inputLabels []string) ([]labels.Matcher, error) {
return matchers, nil
}
// Parse a list of labels (cli arguments)
func parseLabels(inputLabels []string) (client.LabelSet, error) {
labelSet := make(client.LabelSet, len(inputLabels))
for _, l := range inputLabels {
name, value, matchType, err := parse.Input(l)
if err != nil {
return client.LabelSet{}, err
}
if matchType != labels.MatchEqual {
return client.LabelSet{}, errors.New("labels must be specified as key=value pairs")
}
labelSet[client.LabelName(name)] = client.LabelValue(value)
}
return labelSet, nil
}
// Only valid for when you are going to add a silence
func TypeMatchers(matchers []labels.Matcher) (types.Matchers, error) {
typeMatchers := types.Matchers{}