Alerting: Add support for retrieving imported Prometheus Alertmanager configurations (#106864)

This commit is contained in:
Alexander Akhmetov
2025-06-18 10:49:40 +02:00
committed by GitHub
parent 4bfd7b6d7c
commit 8c6df8b449
9 changed files with 550 additions and 151 deletions
@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -10,6 +11,8 @@ import (
"strings"
"time"
amconfig "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
prommodel "github.com/prometheus/common/model"
"gopkg.in/yaml.v3"
@@ -47,6 +50,13 @@ const (
// notificationSettingsHeader is the header that specifies the notification settings to be used for the rules.
// The value should be a JSON-encoded AlertRuleNotificationSettings object.
notificationSettingsHeader = "X-Grafana-Alerting-Notification-Settings"
// mergeMatchersHeader is the header that specifies the merge matchers for imported Alertmanager config.
// The value should be comma-separated key=value pairs, e.g., "environment=production,team=alerting".
mergeMatchersHeader = "X-Grafana-Alerting-Merge-Matchers"
// configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config.
configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier"
)
var (
@@ -113,6 +123,11 @@ type ConvertPrometheusSrv struct {
datasourceCache datasources.CacheService
alertRuleService *provisioning.AlertRuleService
featureToggles featuremgmt.FeatureToggles
am Alertmanager
}
type Alertmanager interface {
GetAlertmanagerConfiguration(ctx context.Context, org int64, withAutogen bool) (apimodels.GettableUserConfig, error)
}
func NewConvertPrometheusSrv(
@@ -122,6 +137,7 @@ func NewConvertPrometheusSrv(
datasourceCache datasources.CacheService,
alertRuleService *provisioning.AlertRuleService,
featureToggles featuremgmt.FeatureToggles,
am Alertmanager,
) *ConvertPrometheusSrv {
return &ConvertPrometheusSrv{
cfg: cfg,
@@ -130,6 +146,7 @@ func NewConvertPrometheusSrv(
datasourceCache: datasourceCache,
alertRuleService: alertRuleService,
featureToggles: featureToggles,
am: am,
}
}
@@ -518,7 +535,47 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
if !srv.featureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingImportAlertmanagerAPI) {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
logger := srv.logger.FromContext(c.Req.Context())
ctx := c.Req.Context()
identifier, err := parseConfigIdentifierHeader(c)
if err != nil {
logger.Error("failed to parse config identifier header", "err", err)
return errorToResponse(err)
}
cfg, err := srv.am.GetAlertmanagerConfiguration(ctx, c.GetOrgID(), false)
if err != nil {
logger.Error("failed to get alertmanager configuration", "err", err)
return errorToResponse(err)
}
var extraCfg *apimodels.ExtraConfiguration
for i := range cfg.ExtraConfigs {
if cfg.ExtraConfigs[i].Identifier == identifier {
extraCfg = &cfg.ExtraConfigs[i]
break
}
}
if extraCfg == nil {
return response.Error(http.StatusNotFound, "Alertmanager configuration not found", nil)
}
respBody := apimodels.AlertmanagerUserConfig{
AlertmanagerConfig: extraCfg.AlertmanagerConfig,
TemplateFiles: extraCfg.TemplateFiles,
}
resp := response.YAML(http.StatusOK, respBody)
resp.SetHeader(configIdentifierHeader, extraCfg.Identifier)
resp.SetHeader(mergeMatchersHeader, formatMergeMatchers(extraCfg.MergeMatchers))
return resp
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
@@ -647,3 +704,55 @@ func parseNotificationSettingsHeader(ctx *contextmodel.ReqContext) ([]models.Not
return notificationSettings, nil
}
// parseMergeMatchersHeader parses the merge matchers header value.
// Expected format: "key1=value1,key2=value2"
func parseMergeMatchersHeader(c *contextmodel.ReqContext) (amconfig.Matchers, error) {
matchersStr := strings.TrimSpace(c.Req.Header.Get(mergeMatchersHeader))
if matchersStr == "" {
return amconfig.Matchers{}, errInvalidHeaderValue(mergeMatchersHeader, errors.New("value cannot be empty"))
}
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"))
}
matchers = append(matchers, &labels.Matcher{
Type: labels.MatchEqual,
Name: key,
Value: value,
})
}
return matchers, nil
}
func formatMergeMatchers(matchers amconfig.Matchers) string {
var pairs []string
for _, matcher := range matchers {
if matcher.Type == labels.MatchEqual {
pairs = append(pairs, fmt.Sprintf("%s=%s", matcher.Name, matcher.Value))
}
}
return strings.Join(pairs, ",")
}
func parseConfigIdentifierHeader(c *contextmodel.ReqContext) (string, error) {
identifier := strings.TrimSpace(c.Req.Header.Get(configIdentifierHeader))
if identifier == "" {
return "", errInvalidHeaderValue(configIdentifierHeader, errors.New("identifier cannot be empty"))
}
return identifier, nil
}