Alerting: Allow more characters in label names so notifications are sent (#38629)

Remove validation for labels to be accepted in the Alertmanager, This helps with datasources that produce non-compatible labels.

Adds an "object_matchers" to alert manager routers so we can support labels names with extended characters beyond prometheus/openmetrics. It only does this for the internal Grafana managed Alert Manager.

This requires a change to alert manager, so for now we use grafana/alertmanager which is a slight fork, with the intention of going back to upstream.

The frontend handles the migration of "matchers" -> "object_matchers" when the route is edited and saved. Once this is done, downgrades will not work old versions will not recognize the "object_matchers".

Co-authored-by: Kyle Brandt <kyle@grafana.com>
Co-authored-by: Nathan Rodman <nathanrodman@gmail.com>
This commit is contained in:
gotjosh
2021-10-04 14:06:40 +01:00
committed by GitHub
parent 706a665240
commit 6572017ec7
16 changed files with 740 additions and 188 deletions

View File

@@ -10,9 +10,11 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"sync"
"time"
"unicode/utf8"
gokit_log "github.com/go-kit/kit/log"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
@@ -39,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/setting"
pb "github.com/prometheus/alertmanager/silence/silencepb"
)
const (
@@ -57,6 +60,24 @@ const (
memoryAlertsGCInterval = 30 * time.Minute
)
func init() {
silence.ValidateMatcher = func(m *pb.Matcher) error {
switch m.Type {
case pb.Matcher_EQUAL, pb.Matcher_NOT_EQUAL:
if !model.LabelValue(m.Pattern).IsValid() {
return fmt.Errorf("invalid label value %q", m.Pattern)
}
case pb.Matcher_REGEXP, pb.Matcher_NOT_REGEXP:
if _, err := regexp.Compile(m.Pattern); err != nil {
return fmt.Errorf("invalid regular expression %q: %s", m.Pattern, err)
}
default:
return fmt.Errorf("unknown matcher type %q", m.Type)
}
return nil
}
}
type ClusterPeer interface {
AddState(string, cluster.State, prometheus.Registerer) cluster.ClusterChannel
Position() int
@@ -392,7 +413,7 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
routingStage[name] = notify.MultiStage{meshStage, silencingStage, inhibitionStage, stage}
}
am.route = dispatch.NewRoute(cfg.AlertmanagerConfig.Route, nil)
am.route = dispatch.NewRoute(cfg.AlertmanagerConfig.Route.AsAMRoute(), nil)
am.dispatcher = dispatch.NewDispatcher(am.alerts, am.route, routingStage, am.marker, am.timeoutFunc, &nilLimits{}, am.gokitLogger, am.dispatcherMetrics)
am.wg.Add(1)
@@ -638,22 +659,14 @@ func validateLabelSet(ls model.LabelSet) error {
return nil
}
// isValidLabelName is ln.IsValid() while additionally allowing spaces.
// The regex for Prometheus data model is ^[a-zA-Z_][a-zA-Z0-9_]*$
// while we will follow ^[a-zA-Z_][a-zA-Z0-9_ ]*$
// isValidLabelName is ln.IsValid() without restrictions other than it can not be empty.
// The regex for Prometheus data model is ^[a-zA-Z_][a-zA-Z0-9_]*$.
func isValidLabelName(ln model.LabelName) bool {
if len(ln) == 0 {
return false
}
for i, b := range ln {
if !((b >= 'a' && b <= 'z') ||
(b >= 'A' && b <= 'Z') ||
b == '_' ||
(i > 0 && (b == ' ' || (b >= '0' && b <= '9')))) {
return false
}
}
return true
return utf8.ValidString(string(ln))
}
// AlertValidationError is the error capturing the validation errors

View File

@@ -208,48 +208,57 @@ func TestPutAlert(t *testing.T) {
}
},
}, {
title: "Invalid labels",
title: "Special characters in labels",
postableAlerts: apimodels.PostableAlerts{
PostableAlerts: []models.PostableAlert{
{
Alert: models.Alert{
Labels: models.LabelSet{"alertname$": "Alert1"},
Labels: models.LabelSet{"alertname$": "Alert1", "az3-- __...++!!!£@@312312": "1"},
},
},
},
},
expError: &AlertValidationError{
Alerts: []models.PostableAlert{
expAlerts: func(now time.Time) []*types.Alert {
return []*types.Alert{
{
Alert: models.Alert{
Labels: models.LabelSet{"alertname$": "Alert1"},
Alert: model.Alert{
Labels: model.LabelSet{"alertname$": "Alert1", "az3-- __...++!!!£@@312312": "1"},
Annotations: model.LabelSet{},
StartsAt: now,
EndsAt: now.Add(defaultResolveTimeout),
GeneratorURL: "",
},
UpdatedAt: now,
Timeout: true,
},
},
Errors: []error{errors.New("invalid label set: invalid name \"alertname$\"")},
}
},
}, {
title: "Invalid annotation",
title: "Special characters in annotations",
postableAlerts: apimodels.PostableAlerts{
PostableAlerts: []models.PostableAlert{
{
Annotations: models.LabelSet{"msg$": "Alert4 annotation"},
Annotations: models.LabelSet{"az3-- __...++!!!£@@312312": "Alert4 annotation"},
Alert: models.Alert{
Labels: models.LabelSet{"alertname": "Alert1"},
Labels: models.LabelSet{"alertname": "Alert4"},
},
},
},
},
expError: &AlertValidationError{
Alerts: []models.PostableAlert{
expAlerts: func(now time.Time) []*types.Alert {
return []*types.Alert{
{
Annotations: models.LabelSet{"msg$": "Alert4 annotation"},
Alert: models.Alert{
Labels: models.LabelSet{"alertname": "Alert1"},
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "Alert4"},
Annotations: model.LabelSet{"az3-- __...++!!!£@@312312": "Alert4 annotation"},
StartsAt: now,
EndsAt: now.Add(defaultResolveTimeout),
GeneratorURL: "",
},
UpdatedAt: now,
Timeout: true,
},
},
Errors: []error{errors.New("invalid annotations: invalid name \"msg$\"")},
}
},
}, {
title: "No labels after removing empty",