Alerting: Support extra labels in the Prometheus conversion API (#109136)

This commit is contained in:
Alexander Akhmetov
2025-08-05 00:03:21 +02:00
committed by GitHub
parent 96c1cd225c
commit f65e501e1b
4 changed files with 212 additions and 15 deletions
@@ -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 {