Alerting: Add compact model for alert rules (#115239)
This commit is contained in:
@@ -457,6 +457,7 @@ type paginationContext struct {
|
||||
labelOptions []ngmodels.LabelOption
|
||||
limitAlertsPerRule int64
|
||||
limitRulesPerGroup int64
|
||||
compact bool
|
||||
}
|
||||
|
||||
// pageResult is the result of fetching and filtering of one page
|
||||
@@ -492,6 +493,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
|
||||
Limit: remainingGroups,
|
||||
RuleLimit: remainingRules,
|
||||
ContinueToken: token,
|
||||
Compact: ctx.compact,
|
||||
}
|
||||
|
||||
ruleList, newToken, err := store.ListAlertRulesByGroup(ctx.opts.Ctx, &byGroupQuery)
|
||||
@@ -519,7 +521,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
|
||||
log, rg.GroupKey, rg.Folder, rg.Rules,
|
||||
ctx.provenanceRecords, ctx.limitAlertsPerRule,
|
||||
ctx.stateFilterSet, ctx.matchers, ctx.labelOptions,
|
||||
ctx.ruleStatusMutator, ctx.alertStateMutator,
|
||||
ctx.ruleStatusMutator, ctx.alertStateMutator, ctx.compact,
|
||||
)
|
||||
ruleGroup.Totals = totals
|
||||
accumulateTotals(result.totalsDelta, totals)
|
||||
@@ -785,6 +787,8 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
}
|
||||
span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet)))
|
||||
|
||||
compact := getBoolWithDefault(opts.Query, "compact", false)
|
||||
span.SetAttributes(attribute.Bool("compact", compact))
|
||||
pagCtx := &paginationContext{
|
||||
opts: opts,
|
||||
provenanceRecords: provenanceRecords,
|
||||
@@ -807,6 +811,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
labelOptions: labelOptions,
|
||||
limitAlertsPerRule: limitAlertsPerRule,
|
||||
limitRulesPerGroup: limitRulesPerGroup,
|
||||
compact: compact,
|
||||
}
|
||||
|
||||
groups, rulesTotals, continueToken, err := paginateRuleGroups(log, store, pagCtx, span, maxGroups, maxRules, nextToken)
|
||||
@@ -959,7 +964,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
|
||||
break
|
||||
}
|
||||
|
||||
ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator)
|
||||
ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator, false)
|
||||
ruleGroup.Totals = totals
|
||||
for k, v := range totals {
|
||||
rulesTotals[k] += v
|
||||
@@ -1110,7 +1115,7 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator) (*apimodels.RuleGroup, map[string]int64) {
|
||||
func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator, compact bool) (*apimodels.RuleGroup, map[string]int64) {
|
||||
newGroup := &apimodels.RuleGroup{
|
||||
Name: groupKey.RuleGroup,
|
||||
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
|
||||
@@ -1126,10 +1131,14 @@ func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFull
|
||||
if prov, exists := provenanceRecords[rule.ResourceID()]; exists {
|
||||
provenance = prov
|
||||
}
|
||||
var query string
|
||||
if !compact {
|
||||
query = ruleToQuery(log, rule)
|
||||
}
|
||||
alertingRule := apimodels.AlertingRule{
|
||||
State: "inactive",
|
||||
Name: rule.Title,
|
||||
Query: ruleToQuery(log, rule),
|
||||
Query: query,
|
||||
QueriedDatasourceUIDs: extractDatasourceUIDs(rule),
|
||||
Duration: rule.For.Seconds(),
|
||||
KeepFiringFor: rule.KeepFiringFor.Seconds(),
|
||||
|
||||
@@ -110,6 +110,12 @@ func (aq *AlertQuery) String() string {
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) setModelProps() error {
|
||||
if aq.Model == nil {
|
||||
// No data to extract, use an empty map.
|
||||
aq.modelProps = map[string]any{}
|
||||
return nil
|
||||
}
|
||||
|
||||
aq.modelProps = make(map[string]any)
|
||||
err := json.Unmarshal(aq.Model, &aq.modelProps)
|
||||
if err != nil {
|
||||
|
||||
@@ -1022,6 +1022,7 @@ type ListAlertRulesExtendedQuery struct {
|
||||
Limit int64
|
||||
RuleLimit int64
|
||||
ContinueToken string
|
||||
Compact bool
|
||||
}
|
||||
|
||||
// CountAlertRulesQuery is the query for counting alert rules
|
||||
|
||||
@@ -631,7 +631,13 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis
|
||||
continue
|
||||
}
|
||||
|
||||
converted, err := alertRuleToModelsAlertRule(*rule, st.Logger)
|
||||
var converted ngmodels.AlertRule
|
||||
if query.Compact {
|
||||
converted, err = alertRuleToModelsAlertRuleCompact(*rule, st.Logger)
|
||||
} else {
|
||||
converted, err = alertRuleToModelsAlertRule(*rule, st.Logger)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "ListAlertRulesByGroup", "error", err)
|
||||
continue
|
||||
|
||||
@@ -10,11 +10,38 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
// We only care about the data source UIDs.
|
||||
type compactQuery struct {
|
||||
DatasourceUID string `json:"datasourceUid"`
|
||||
}
|
||||
|
||||
func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, error) {
|
||||
return convertAlertRuleToModel(ar, l, false)
|
||||
}
|
||||
|
||||
// alertRuleToModelsAlertRuleCompact transforms an alertRule to a models.AlertRule
|
||||
// ignoring alert queries (except for data source UIDs), notification settings, and metadata.
|
||||
func alertRuleToModelsAlertRuleCompact(ar alertRule, l log.Logger) (models.AlertRule, error) {
|
||||
return convertAlertRuleToModel(ar, l, true)
|
||||
}
|
||||
|
||||
// convertAlertRuleToModel creates a models.AlertRule from an alertRule.
|
||||
// When 'compact' is set to 'true', it skips parsing the alert queries (except for the data source UID), notification
|
||||
// settings, and metadata, thus reducing the number of JSON serializations needed.
|
||||
func convertAlertRuleToModel(ar alertRule, l log.Logger, compact bool) (models.AlertRule, error) {
|
||||
var data []models.AlertQuery
|
||||
err := json.Unmarshal([]byte(ar.Data), &data)
|
||||
if err != nil {
|
||||
return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err)
|
||||
if compact {
|
||||
var cqs []compactQuery
|
||||
if err := json.Unmarshal([]byte(ar.Data), &cqs); err != nil {
|
||||
return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err)
|
||||
}
|
||||
for _, cq := range cqs {
|
||||
data = append(data, models.AlertQuery{DatasourceUID: cq.DatasourceUID})
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal([]byte(ar.Data), &data); err != nil {
|
||||
return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
result := models.AlertRule{
|
||||
@@ -52,6 +79,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e
|
||||
result.UpdatedBy = util.Pointer(models.UserUID(*ar.UpdatedBy))
|
||||
}
|
||||
|
||||
var err error
|
||||
if ar.NoDataState != "" {
|
||||
result.NoDataState, err = models.NoDataStateFromString(ar.NoDataState)
|
||||
if err != nil {
|
||||
@@ -90,7 +118,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e
|
||||
}
|
||||
}
|
||||
|
||||
if ar.NotificationSettings != "" {
|
||||
if !compact && ar.NotificationSettings != "" {
|
||||
ns, err := parseNotificationSettings(ar.NotificationSettings)
|
||||
if err != nil {
|
||||
return models.AlertRule{}, fmt.Errorf("failed to parse notification settings: %w", err)
|
||||
@@ -98,7 +126,7 @@ func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, e
|
||||
result.NotificationSettings = ns
|
||||
}
|
||||
|
||||
if ar.Metadata != "" {
|
||||
if !compact && ar.Metadata != "" {
|
||||
err = json.Unmarshal([]byte(ar.Metadata), &result.Metadata)
|
||||
if err != nil {
|
||||
return models.AlertRule{}, fmt.Errorf("failed to metadata: %w", err)
|
||||
|
||||
@@ -65,6 +65,85 @@ func TestAlertRuleToModelsAlertRule(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertRuleToModelsAlertRuleCompact(t *testing.T) {
|
||||
t.Run("should only extract datasource UIDs in compact mode", func(t *testing.T) {
|
||||
rule := alertRule{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
UID: "test-uid",
|
||||
Title: "Test Rule",
|
||||
Condition: "A",
|
||||
Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}},{"datasourceUid":"ds2","refId":"B","queryType":"test","model":{"expr":"down"}}]`,
|
||||
IntervalSeconds: 60,
|
||||
Version: 1,
|
||||
NamespaceUID: "ns-uid",
|
||||
RuleGroup: "test-group",
|
||||
NoDataState: "NoData",
|
||||
ExecErrState: "Error",
|
||||
NotificationSettings: `[{"receiver":"test-receiver"}]`,
|
||||
Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`,
|
||||
}
|
||||
|
||||
compactResult, err := alertRuleToModelsAlertRuleCompact(rule, &logtest.Fake{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have datasource UIDs.
|
||||
require.Len(t, compactResult.Data, 2)
|
||||
require.Equal(t, "ds1", compactResult.Data[0].DatasourceUID)
|
||||
require.Equal(t, "ds2", compactResult.Data[1].DatasourceUID)
|
||||
|
||||
// But should not have full query data (RefID, QueryType, Model should be empty).
|
||||
require.Empty(t, compactResult.Data[0].RefID)
|
||||
require.Empty(t, compactResult.Data[0].QueryType)
|
||||
require.Nil(t, compactResult.Data[0].Model)
|
||||
require.Empty(t, compactResult.Data[1].RefID)
|
||||
require.Empty(t, compactResult.Data[1].QueryType)
|
||||
require.Nil(t, compactResult.Data[1].Model)
|
||||
|
||||
// Should not have notification settings.
|
||||
require.Empty(t, compactResult.NotificationSettings)
|
||||
|
||||
// Should not have metadata (should be zero value).
|
||||
require.Equal(t, ngmodels.AlertRuleMetadata{}, compactResult.Metadata)
|
||||
})
|
||||
|
||||
t.Run("should parse full data in non-compact mode", func(t *testing.T) {
|
||||
rule := alertRule{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
UID: "test-uid",
|
||||
Title: "Test Rule",
|
||||
Condition: "A",
|
||||
Data: `[{"datasourceUid":"ds1","refId":"A","queryType":"test","model":{"expr":"up"}},{"datasourceUid":"ds2","refId":"B","queryType":"test","model":{"expr":"down"}}]`,
|
||||
IntervalSeconds: 60,
|
||||
Version: 1,
|
||||
NamespaceUID: "ns-uid",
|
||||
RuleGroup: "test-group",
|
||||
NoDataState: "NoData",
|
||||
ExecErrState: "Error",
|
||||
NotificationSettings: `[{"receiver":"test-receiver"}]`,
|
||||
Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`,
|
||||
}
|
||||
|
||||
fullResult, err := alertRuleToModelsAlertRule(rule, &logtest.Fake{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have full query data.
|
||||
require.Len(t, fullResult.Data, 2)
|
||||
require.Equal(t, "ds1", fullResult.Data[0].DatasourceUID)
|
||||
require.Equal(t, "A", fullResult.Data[0].RefID)
|
||||
require.Equal(t, "test", fullResult.Data[0].QueryType)
|
||||
require.NotNil(t, fullResult.Data[0].Model)
|
||||
|
||||
// Should have notification settings.
|
||||
require.Len(t, fullResult.NotificationSettings, 1)
|
||||
require.Equal(t, "test-receiver", fullResult.NotificationSettings[0].Receiver)
|
||||
|
||||
// Should have metadata (metadata is parsed from JSON to struct).
|
||||
require.NotEqual(t, ngmodels.AlertRuleMetadata{}, fullResult.Metadata)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlertRuleVersionToAlertRule(t *testing.T) {
|
||||
g := ngmodels.RuleGen
|
||||
|
||||
|
||||
Reference in New Issue
Block a user