Alerting: Implement setting of version message in convert API. (#114331)

Part 2 of adding version messages to the `alert_rule_version` able. This allows
setting the message via a header when using the Prometheus conversion API,
which can be useful for e.g. linking changes back to source control.
This commit is contained in:
Steve Simpson
2025-11-25 11:20:28 +01:00
committed by GitHub
parent b57e6383e4
commit d50e2a94a3
16 changed files with 212 additions and 101 deletions
@@ -63,6 +63,9 @@ const (
// configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config.
configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier"
defaultConfigIdentifier = "default"
// versionMessageHeader is the header that specifies an optional message for rule versions.
versionMessageHeader = "X-Grafana-Alerting-Version-Message"
)
var (
@@ -407,6 +410,11 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
return errorToResponse(err)
}
versionMessage, err := parseVersionMessageHeader(c)
if err != nil {
logger.Error("Failed to parse version message header", "error", err)
return errorToResponse(err)
}
// 2. Convert Prometheus Rules to GMA
grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces))
for ns, rgs := range promNamespaces {
@@ -444,12 +452,13 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
return errorToResponse(err)
}
grafanaGroups = append(grafanaGroups, grafanaGroup)
}
}
// 3. Update the GMA Rules in the DB
err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance)
err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance, versionMessage)
if err != nil {
logger.Error("Failed to replace rule groups", "error", err)
return errorToResponse(err)
@@ -856,6 +865,16 @@ func parseExtraLabelsHeader(c *contextmodel.ReqContext) (map[string]string, erro
return parseKeyValuePairs(labelsStr, extraLabelsHeader)
}
// parseVersionMessageHeader obtains and validates the message header value.
func parseVersionMessageHeader(c *contextmodel.ReqContext) (string, error) {
str := strings.TrimSpace(c.Req.Header.Get(versionMessageHeader))
// Limit message to the same as the dashboards message.
if len(str) > 500 {
return "", errInvalidHeaderValue(versionMessageHeader, errors.New("must be less than 500 characters"))
}
return str, nil
}
func formatMergeMatchers(matchers amconfig.Matchers) string {
var pairs []string
for _, matcher := range matchers {
@@ -506,6 +506,68 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
}
})
t.Run("with version message header should pass message to rule store", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
folderService := foldertest.NewFakeService()
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
// Create a folder in the root
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
makeGroup := func(alertname string) apimodels.PrometheusRuleGroup {
return apimodels.PrometheusRuleGroup{
Name: "Test Group",
Interval: prommodel.Duration(1 * time.Minute),
Rules: []apimodels.PrometheusRule{
{
Alert: alertname,
Expr: "up == 0",
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
Labels: map[string]string{
"severity": "critical",
},
},
},
}
}
// Create a rule with the X-Grafana-Alerting-Version-Message to check it's passed to InsertRule.
rc1 := createRequestCtx()
rc1.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #1")
response1 := srv.RouteConvertPrometheusPostRuleGroup(rc1, fldr.Title, makeGroup("AlertV1"))
require.Equal(t, http.StatusAccepted, response1.Status())
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
a, ok := cmd.([]models.InsertRule)
return a, ok
})
require.Len(t, inserts, 1)
cmd1 := inserts[0].([]models.InsertRule)
require.Len(t, cmd1, 1)
require.Equal(t, "version #1", cmd1[0].Message)
// Now update the rule to check it gets passed to UpdateRule.
rc2 := createRequestCtx()
rc2.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #2")
response2 := srv.RouteConvertPrometheusPostRuleGroup(rc2, fldr.Title, makeGroup("AlertV2"))
require.Equal(t, http.StatusAccepted, response2.Status())
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
a, ok := cmd.([]models.UpdateRule)
return a, ok
})
require.Len(t, updates, 1)
cmd2 := updates[0].([]models.UpdateRule)
require.Len(t, cmd2, 1)
require.Equal(t, "version #2", cmd2[0].Message)
})
t.Run("returns error when target datasource does not exist", func(t *testing.T) {
srv, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
+5 -2
View File
@@ -75,7 +75,7 @@ type AlertRuleService interface {
UpdateAlertRule(ctx context.Context, user identity.Requester, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error)
DeleteAlertRule(ctx context.Context, user identity.Requester, ruleUID string, provenance alerting_models.Provenance) error
GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error)
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance, message string) error
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error
DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error
GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error)
@@ -492,7 +492,10 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a
ErrResp(http.StatusBadRequest, err, "")
}
provenance := determineProvenance(c)
err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance))
// TODO: https://github.com/grafana/grafana/issues/114197
// Support passing change messages.
changeMessage := ""
err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance), changeMessage)
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
return ErrResp(http.StatusBadRequest, err, "")
}
+2 -2
View File
@@ -576,9 +576,9 @@ func (srv RulerSrv) performUpdateAlertRules(ctx context.Context, c *contextmodel
}
if len(finalChanges.New) > 0 {
inserts := make([]ngmodels.AlertRule, 0, len(finalChanges.New))
inserts := make([]ngmodels.InsertRule, 0, len(finalChanges.New))
for _, rule := range finalChanges.New {
inserts = append(inserts, *rule)
inserts = append(inserts, ngmodels.InsertRule{AlertRule: *rule})
}
added, err := srv.store.InsertAlertRules(tranCtx, ngmodels.NewUserUID(c.SignedInUser), inserts)
if err != nil {
+1 -1
View File
@@ -28,7 +28,7 @@ type RuleStore interface {
// InsertAlertRules will insert all alert rules passed into the function
// and return the map of uuid to id.
InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error)
InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.InsertRule) ([]ngmodels.AlertRuleKeyWithId, error)
UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.UpdateRule) error
DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, permanently bool, ruleUID ...string) error
DeleteRuleFromTrashByGUID(ctx context.Context, orgID int64, ruleGUID string) (int64, error)