Alerting: Add support for retrieving imported Prometheus Alertmanager configurations (#106864)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user