diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 71e25d4c963..75ec6c901fd 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -2169,6 +2169,57 @@ func TestRouteGetRuleStatuses(t *testing.T) { }) }) + t.Run("compact mode with receiver_name filter returns only matching rules", func(t *testing.T) { + fakeStore, _, api := setupAPI(t) + + ruleA := gen.With( + gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + NamespaceUID: "folder-1", + RuleGroup: "group-1", + OrgID: orgID, + }), + gen.WithNotificationSettings( + ngmodels.NotificationSettings{ + Receiver: "receiver-a", + GroupBy: []string{"alertname"}, + }, + ), + ).GenerateRef() + fakeStore.PutRule(context.Background(), ruleA) + + ruleB := gen.With( + gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + NamespaceUID: "folder-2", + RuleGroup: "group-2", + OrgID: orgID, + }), + gen.WithNotificationSettings( + ngmodels.NotificationSettings{ + Receiver: "receiver-b", + GroupBy: []string{"alertname"}, + }, + ), + ).GenerateRef() + fakeStore.PutRule(context.Background(), ruleB) + r, err := http.NewRequest("GET", "/api/v1/rules?compact=true&receiver_name=receiver-a", nil) + require.NoError(t, err) + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: r}, + SignedInUser: &user.SignedInUser{ + OrgID: orgID, + Permissions: queryPermissions, + }, + } + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + var res apimodels.RuleResponse + require.NoError(t, json.Unmarshal(resp.Body(), &res)) + + require.Len(t, res.Data.RuleGroups, 1) + require.Equal(t, "group-1", res.Data.RuleGroups[0].Name) + require.Empty(t, res.Data.RuleGroups[0].Rules[0].Query, "Query should be empty in compact mode") + }) + t.Run("provenance as expected", func(t *testing.T) { fakeStore, fakeAIM, api, provStore := setupAPIFull(t) // Rule without provenance diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 5957e7026b2..30856587794 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -642,6 +642,23 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis _ = rows.Close() }() + opts := AlertRuleConvertOptions{} + if query.Compact { + opts.ExcludeAlertQueries = true + opts.ExcludeNotificationSettings = true + opts.ExcludeMetadata = true + + if query.ReceiverName != "" || query.TimeIntervalName != "" { + // Need NotificationSettings for these filters + opts.ExcludeNotificationSettings = false + } + + if query.HasPrometheusRuleDefinition != nil { + // Need Metadata for this filter + opts.ExcludeMetadata = false + } + } + // Process rules and implement per-group pagination var groupsFetched int64 var rulesFetched int64 @@ -653,12 +670,7 @@ func (st DBstore) ListAlertRulesByGroup(ctx context.Context, query *ngmodels.Lis continue } - var converted ngmodels.AlertRule - if query.Compact { - converted, err = alertRuleToModelsAlertRuleCompact(*rule, st.Logger) - } else { - converted, err = alertRuleToModelsAlertRule(*rule, st.Logger) - } + converted, err := convertAlertRuleToModel(*rule, st.Logger, opts) if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "ListAlertRulesByGroup", "error", err) diff --git a/pkg/services/ngalert/store/compat.go b/pkg/services/ngalert/store/compat.go index fbb69addc72..57dcf411853 100644 --- a/pkg/services/ngalert/store/compat.go +++ b/pkg/services/ngalert/store/compat.go @@ -15,22 +15,23 @@ type compactQuery struct { DatasourceUID string `json:"datasourceUid"` } -func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, error) { - return convertAlertRuleToModel(ar, l, false) +// AlertRuleConvertOptions controls which fields to parse during conversion from alertRule to models.AlertRule. +// By default all fields are included. Set Exclude* to true to skip parsing expensive fields. +type AlertRuleConvertOptions struct { + ExcludeAlertQueries bool // Only parse datasource UIDs from queries + ExcludeNotificationSettings bool + ExcludeMetadata bool } -// 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) +func alertRuleToModelsAlertRule(ar alertRule, l log.Logger) (models.AlertRule, error) { + return convertAlertRuleToModel(ar, l, AlertRuleConvertOptions{}) } // 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) { +// opts.Exclude* fields control which expensive fields to skip parsing, reducing JSON serializations. +func convertAlertRuleToModel(ar alertRule, l log.Logger, opts AlertRuleConvertOptions) (models.AlertRule, error) { var data []models.AlertQuery - if compact { + if opts.ExcludeAlertQueries { var cqs []compactQuery if err := json.Unmarshal([]byte(ar.Data), &cqs); err != nil { return models.AlertRule{}, fmt.Errorf("failed to parse data: %w", err) @@ -118,7 +119,7 @@ func convertAlertRuleToModel(ar alertRule, l log.Logger, compact bool) (models.A } } - if !compact && ar.NotificationSettings != "" { + if !opts.ExcludeNotificationSettings && ar.NotificationSettings != "" { ns, err := parseNotificationSettings(ar.NotificationSettings) if err != nil { return models.AlertRule{}, fmt.Errorf("failed to parse notification settings: %w", err) @@ -126,7 +127,7 @@ func convertAlertRuleToModel(ar alertRule, l log.Logger, compact bool) (models.A result.NotificationSettings = ns } - if !compact && ar.Metadata != "" { + if !opts.ExcludeMetadata && ar.Metadata != "" { err = json.Unmarshal([]byte(ar.Metadata), &result.Metadata) if err != nil { return models.AlertRule{}, fmt.Errorf("failed to metadata: %w", err) diff --git a/pkg/services/ngalert/store/compat_test.go b/pkg/services/ngalert/store/compat_test.go index ef80f51d668..d4aab40931f 100644 --- a/pkg/services/ngalert/store/compat_test.go +++ b/pkg/services/ngalert/store/compat_test.go @@ -84,7 +84,11 @@ func TestAlertRuleToModelsAlertRuleCompact(t *testing.T) { Metadata: `{"editor_settings":{"simplified_query_and_expressions_section":true}}`, } - compactResult, err := alertRuleToModelsAlertRuleCompact(rule, &logtest.Fake{}) + compactResult, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{ + ExcludeAlertQueries: true, + ExcludeNotificationSettings: true, + ExcludeMetadata: true, + }) require.NoError(t, err) // Should have datasource UIDs. @@ -142,6 +146,82 @@ func TestAlertRuleToModelsAlertRuleCompact(t *testing.T) { // Should have metadata (metadata is parsed from JSON to struct). require.NotEqual(t, ngmodels.AlertRuleMetadata{}, fullResult.Metadata) }) + + t.Run("compact mode with notification settings included for filtering", 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"}}]`, + 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}}`, + } + + result, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{ + ExcludeAlertQueries: true, + ExcludeNotificationSettings: false, + ExcludeMetadata: true, + }) + require.NoError(t, err) + + // Should have compact query data (only datasource UIDs). + require.Len(t, result.Data, 1) + require.Equal(t, "ds1", result.Data[0].DatasourceUID) + require.Empty(t, result.Data[0].RefID) + + // Should have notification settings for filtering. + require.Len(t, result.NotificationSettings, 1) + require.Equal(t, "test-receiver", result.NotificationSettings[0].Receiver) + + // Should not have metadata. + require.Equal(t, ngmodels.AlertRuleMetadata{}, result.Metadata) + }) + + t.Run("compact mode with metadata included for filtering", 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"}}]`, + IntervalSeconds: 60, + Version: 1, + NamespaceUID: "ns-uid", + RuleGroup: "test-group", + NoDataState: "NoData", + ExecErrState: "Error", + NotificationSettings: `[{"receiver":"test-receiver"}]`, + Metadata: `{"prometheus_style_rule":{"original_rule_definition":"alert: TestAlert\n expr: rate(metric[5m]) > 1"}}`, + } + + result, err := convertAlertRuleToModel(rule, &logtest.Fake{}, AlertRuleConvertOptions{ + ExcludeAlertQueries: true, + ExcludeNotificationSettings: true, + ExcludeMetadata: false, + }) + require.NoError(t, err) + + // Should have compact query data (only datasource UIDs). + require.Len(t, result.Data, 1) + require.Equal(t, "ds1", result.Data[0].DatasourceUID) + require.Empty(t, result.Data[0].RefID) + + // Should not have notification settings. + require.Empty(t, result.NotificationSettings) + + // Should have metadata for filtering. + require.NotEqual(t, ngmodels.AlertRuleMetadata{}, result.Metadata) + require.True(t, result.HasPrometheusRuleDefinition()) + }) } func TestAlertRuleVersionToAlertRule(t *testing.T) {