Alerting: Support extra labels in the Prometheus conversion API (#109136)
This commit is contained in:
@@ -56,6 +56,10 @@ const (
|
||||
// The value should be comma-separated key=value pairs, e.g., "environment=production,team=alerting".
|
||||
mergeMatchersHeader = "X-Grafana-Alerting-Merge-Matchers"
|
||||
|
||||
// extraLabelsHeader is the header that specifies extra labels to be added to all imported rules.
|
||||
// The value should be comma-separated key=value pairs, e.g., "environment=production,team=alerting".
|
||||
extraLabelsHeader = "X-Grafana-Alerting-Extra-Labels"
|
||||
|
||||
// configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config.
|
||||
configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier"
|
||||
defaultConfigIdentifier = "default"
|
||||
@@ -397,6 +401,12 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
extraLabels, err := parseExtraLabelsHeader(c)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse extra labels header", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
// 2. Convert Prometheus Rules to GMA
|
||||
grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces))
|
||||
for ns, rgs := range promNamespaces {
|
||||
@@ -427,6 +437,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
|
||||
pauseAlertRules,
|
||||
keepOriginalRuleDefinition,
|
||||
notificationSettings,
|
||||
extraLabels,
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -477,6 +488,7 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
||||
pauseAlertRules bool,
|
||||
keepOriginalRuleDefinition bool,
|
||||
notificationSettings []models.NotificationSettings,
|
||||
extraLabels map[string]string,
|
||||
logger log.Logger,
|
||||
) (*models.AlertRuleGroup, error) {
|
||||
logger.Info("Converting Prometheus rules to Grafana rules", "rules", len(promGroup.Rules), "folder_uid", namespaceUID, "datasource_uid", ds.UID, "datasource_type", ds.Type)
|
||||
@@ -518,6 +530,7 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
|
||||
KeepOriginalRuleDefinition: util.Pointer(keepOriginalRuleDefinition),
|
||||
EvaluationOffset: &srv.cfg.PrometheusConversion.RuleQueryOffset,
|
||||
NotificationSettings: notificationSettings,
|
||||
ExtraLabels: extraLabels,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -758,6 +771,35 @@ func parseNotificationSettingsHeader(ctx *contextmodel.ReqContext) ([]models.Not
|
||||
return notificationSettings, nil
|
||||
}
|
||||
|
||||
// parseKeyValuePairs parses a comma-separated list of key=value pairs.
|
||||
// Expected format: "key1=value1,key2=value2"
|
||||
func parseKeyValuePairs(input string, headerName string) (map[string]string, error) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
|
||||
for pair := range strings.SplitSeq(input, ",") {
|
||||
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errInvalidHeaderValue(headerName, errors.New("format should be 'key=value,key2=value2'"))
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if key == "" || value == "" {
|
||||
return nil, errInvalidHeaderValue(headerName, errors.New("keys and values cannot be empty"))
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseMergeMatchersHeader parses the merge matchers header value.
|
||||
// Expected format: "key1=value1,key2=value2"
|
||||
func parseMergeMatchersHeader(c *contextmodel.ReqContext) (amconfig.Matchers, error) {
|
||||
@@ -767,21 +809,13 @@ func parseMergeMatchersHeader(c *contextmodel.ReqContext) (amconfig.Matchers, er
|
||||
return amconfig.Matchers{}, errInvalidHeaderValue(mergeMatchersHeader, errors.New("value cannot be empty"))
|
||||
}
|
||||
|
||||
kvPairs, err := parseKeyValuePairs(matchersStr, mergeMatchersHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matchers := amconfig.Matchers{}
|
||||
|
||||
for pair := range strings.SplitSeq(matchersStr, ",") {
|
||||
parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errInvalidHeaderValue(mergeMatchersHeader, errors.New("format should be 'key=value,key2=value2'"))
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if key == "" || value == "" {
|
||||
return nil, errInvalidHeaderValue(mergeMatchersHeader, errors.New("keys and values cannot be empty"))
|
||||
}
|
||||
|
||||
for key, value := range kvPairs {
|
||||
matchers = append(matchers, &labels.Matcher{
|
||||
Type: labels.MatchEqual,
|
||||
Name: key,
|
||||
@@ -792,6 +826,13 @@ func parseMergeMatchersHeader(c *contextmodel.ReqContext) (amconfig.Matchers, er
|
||||
return matchers, nil
|
||||
}
|
||||
|
||||
// parseExtraLabelsHeader parses the extra labels header value.
|
||||
// Expected format: "key1=value1,key2=value2"
|
||||
func parseExtraLabelsHeader(c *contextmodel.ReqContext) (map[string]string, error) {
|
||||
labelsStr := strings.TrimSpace(c.Req.Header.Get(extraLabelsHeader))
|
||||
return parseKeyValuePairs(labelsStr, extraLabelsHeader)
|
||||
}
|
||||
|
||||
func formatMergeMatchers(matchers amconfig.Matchers) string {
|
||||
var pairs []string
|
||||
for _, matcher := range matchers {
|
||||
|
||||
Reference in New Issue
Block a user