Alerting: Rule version history API (#99041)

* implement store method to read rule versions

* implement request handler

* declare a new endpoint

* fix fake to return correct response

* add tests

* add integration tests

* rename history to versions

* apply diff from swagger CI step

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>

---------

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Yuri Tseretyan
2025-02-03 13:26:18 -05:00
committed by GitHub
parent 8a259ecafa
commit ac41c19350
20 changed files with 772 additions and 16 deletions
+30
View File
@@ -118,6 +118,36 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler
return result, err
}
func (st DBstore) GetAlertRuleVersions(ctx context.Context, key ngmodels.AlertRuleKey) ([]*ngmodels.AlertRule, error) {
alertRules := make([]*ngmodels.AlertRule, 0)
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_uid = ?", key.OrgID, key.UID).Desc("id").Rows(new(alertRuleVersion))
if err != nil {
return err
}
// Deserialize each rule separately in case any of them contain invalid JSON.
for rows.Next() {
rule := new(alertRuleVersion)
err = rows.Scan(rule)
if err != nil {
st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err)
continue
}
converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger)
if err != nil {
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID)
continue
}
alertRules = append(alertRules, &converted)
}
return nil
})
if err != nil {
return nil, err
}
return alertRules, nil
}
// GetRuleByID retrieves models.AlertRule by ID.
// It returns models.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
func (st DBstore) GetRuleByID(ctx context.Context, query ngmodels.GetAlertRuleByIDQuery) (result *ngmodels.AlertRule, err error) {
+31
View File
@@ -204,3 +204,34 @@ func alertRuleToAlertRuleVersion(rule alertRule) alertRuleVersion {
Metadata: rule.Metadata,
}
}
func alertRuleVersionToAlertRule(version alertRuleVersion) alertRule {
return alertRule{
ID: version.ID,
OrgID: version.RuleOrgID,
Title: version.Title,
Condition: version.Condition,
Data: version.Data,
Updated: version.Created,
UpdatedBy: version.CreatedBy,
IntervalSeconds: version.IntervalSeconds,
Version: version.Version,
UID: version.RuleUID,
NamespaceUID: version.RuleNamespaceUID,
// Versions do not store Dashboard\Panel as separate column.
// However, these fields are part of annotations and information in these fields is redundant
DashboardUID: nil,
PanelID: nil,
RuleGroup: version.RuleGroup,
RuleGroupIndex: version.RuleGroupIndex,
Record: version.Record,
NoDataState: version.NoDataState,
ExecErrState: version.ExecErrState,
For: version.For,
Annotations: version.Annotations,
Labels: version.Labels,
IsPaused: version.IsPaused,
NotificationSettings: version.NotificationSettings,
Metadata: version.Metadata,
}
}
+17
View File
@@ -43,3 +43,20 @@ func TestAlertRuleToModelsAlertRule(t *testing.T) {
require.Equal(t, ngmodels.ErrorErrState, converted.ExecErrState)
})
}
func TestAlertRuleVersionToAlertRule(t *testing.T) {
g := ngmodels.RuleGen
t.Run("make sure no data is lost between conversions", func(t *testing.T) {
for _, rule := range g.GenerateMany(100) {
// ignore fields
rule.DashboardUID = nil
rule.PanelID = nil
r, err := alertRuleFromModelsAlertRule(rule)
require.NoError(t, err)
r2 := alertRuleVersionToAlertRule(alertRuleToAlertRuleVersion(r))
require.Equal(t, r, r2)
}
})
}