Compare commits
1 Commits
sriram/SQL
...
alexander-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
285a051c49 |
@@ -328,6 +328,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingUIUseFullyCompatBackendFilters?: boolean;
|
||||
/**
|
||||
* Enables the UI to use limit_rules=0 when calling the API and rules information is not needed
|
||||
*/
|
||||
alertingUIUseLimitRules?: boolean;
|
||||
/**
|
||||
* Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.
|
||||
*/
|
||||
alertmanagerRemotePrimary?: boolean;
|
||||
|
||||
@@ -527,6 +527,13 @@ var (
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "alertingUIUseLimitRules",
|
||||
Description: "Enables the UI to use limit_rules=0 when calling the API and rules information is not needed",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "alertmanagerRemotePrimary",
|
||||
Description: "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.",
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -73,6 +73,7 @@ alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,fal
|
||||
alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingUIUseBackendFilters,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingUIUseFullyCompatBackendFilters,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingUIUseLimitRules,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false
|
||||
annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
|
||||
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -247,6 +247,10 @@ const (
|
||||
// Enables the UI to use rules backend-side filters 100% compatible with the frontend filters
|
||||
FlagAlertingUIUseFullyCompatBackendFilters = "alertingUIUseFullyCompatBackendFilters"
|
||||
|
||||
// FlagAlertingUIUseLimitRules
|
||||
// Enables the UI to use limit_rules=0 when calling the API and rules information is not needed
|
||||
FlagAlertingUIUseLimitRules = "alertingUIUseLimitRules"
|
||||
|
||||
// FlagAlertmanagerRemotePrimary
|
||||
// Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.
|
||||
FlagAlertmanagerRemotePrimary = "alertmanagerRemotePrimary"
|
||||
|
||||
13
pkg/services/featuremgmt/toggles_gen.json
generated
13
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -566,6 +566,19 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingUIUseLimitRules",
|
||||
"resourceVersion": "1765891213465",
|
||||
"creationTimestamp": "2025-12-16T13:20:13Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the UI to use limit_rules=0 when calling the API and rules information is not needed",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingUseNewSimplifiedRoutingHashAlgorithm",
|
||||
|
||||
@@ -1518,6 +1518,81 @@ func TestRouteGetRuleStatuses(t *testing.T) {
|
||||
require.Len(t, res.Data.RuleGroups[0].Rules, 1)
|
||||
require.Len(t, res.Data.RuleGroups[1].Rules, 1)
|
||||
})
|
||||
|
||||
t.Run("with limit_rules=0 returns empty rule groups", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=0", 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))
|
||||
|
||||
// Should return rule groups but with no rules
|
||||
require.Len(t, res.Data.RuleGroups, 2)
|
||||
require.Len(t, res.Data.RuleGroups[0].Rules, 0)
|
||||
require.Len(t, res.Data.RuleGroups[1].Rules, 0)
|
||||
})
|
||||
|
||||
t.Run("with limit_rules=0 and state filter returns error", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=0&state=firing", 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.StatusBadRequest, resp.Status())
|
||||
var res apimodels.RuleResponse
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||||
require.Equal(t, "error", res.Status)
|
||||
require.Contains(t, res.Error, "limit_rules=0 cannot be used together with state or health filters")
|
||||
})
|
||||
|
||||
t.Run("with limit_rules=0 and health filter returns error", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=0&health=ok", 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.StatusBadRequest, resp.Status())
|
||||
var res apimodels.RuleResponse
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||||
require.Equal(t, "error", res.Status)
|
||||
require.Contains(t, res.Error, "limit_rules=0 cannot be used together with state or health filters")
|
||||
})
|
||||
|
||||
t.Run("then with limit_rules=0 and both state and health filters returns error", func(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "/api/v1/rules?limit_rules=0&state=firing&health=ok", 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.StatusBadRequest, resp.Status())
|
||||
var res apimodels.RuleResponse
|
||||
require.NoError(t, json.Unmarshal(resp.Body(), &res))
|
||||
require.Equal(t, "error", res.Status)
|
||||
require.Contains(t, res.Error, "limit_rules=0 cannot be used together with state or health filters")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("test with limit alerts", func(t *testing.T) {
|
||||
|
||||
@@ -573,8 +573,11 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
|
||||
ctx.stateFilterSet, ctx.matchers, ctx.labelOptions,
|
||||
ctx.ruleStatusMutator, ctx.alertStateMutator, ctx.compact,
|
||||
)
|
||||
ruleGroup.Totals = totals
|
||||
accumulateTotals(result.totalsDelta, totals)
|
||||
// Only set totals if limit_rules is not 0
|
||||
if ctx.limitRulesPerGroup != 0 {
|
||||
ruleGroup.Totals = totals
|
||||
accumulateTotals(result.totalsDelta, totals)
|
||||
}
|
||||
|
||||
if len(ctx.stateFilterSet) > 0 {
|
||||
filterRulesByState(ruleGroup, ctx.stateFilterSet)
|
||||
@@ -590,7 +593,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
|
||||
ruleGroup.Rules = ruleGroup.Rules[0:ctx.limitRulesPerGroup]
|
||||
}
|
||||
|
||||
if len(ruleGroup.Rules) > 0 {
|
||||
if len(ruleGroup.Rules) > 0 || ctx.limitRulesPerGroup == 0 {
|
||||
result.groups = append(result.groups, *ruleGroup)
|
||||
}
|
||||
}
|
||||
@@ -668,6 +671,7 @@ func paginateRuleGroups(log log.Logger, store ListAlertRulesStoreV2, ctx *pagina
|
||||
return allGroups, rulesTotals, continueToken, nil
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceStore ProvenanceStore) apimodels.RuleResponse {
|
||||
ctx, span := tracer.Start(opts.Ctx, "api.prometheus.PrepareRuleGroupStatusesV2")
|
||||
defer span.End()
|
||||
@@ -746,6 +750,13 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
attribute.StringSlice("health_filter", slices.Collect(maps.Keys(healthFilterSet))),
|
||||
)
|
||||
|
||||
if limitRulesPerGroup == 0 && (len(stateFilterSet) > 0 || len(healthFilterSet) > 0) {
|
||||
ruleResponse.Status = "error"
|
||||
ruleResponse.Error = "limit_rules=0 cannot be used together with state or health filters"
|
||||
ruleResponse.ErrorType = apiv1.ErrBadData
|
||||
return ruleResponse
|
||||
}
|
||||
|
||||
var labelOptions []ngmodels.LabelOption
|
||||
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
|
||||
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
|
||||
@@ -896,8 +907,8 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
|
||||
ruleResponse.Data.RuleGroups = groups
|
||||
ruleResponse.Data.NextToken = continueToken
|
||||
|
||||
// Only return Totals if there is no pagination
|
||||
if maxGroups == -1 && maxRules == -1 {
|
||||
// Only return Totals if there is no pagination and limit_rules is not 0
|
||||
if maxGroups == -1 && maxRules == -1 && limitRulesPerGroup != 0 {
|
||||
ruleResponse.Data.Totals = rulesTotals
|
||||
}
|
||||
|
||||
@@ -944,6 +955,10 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
|
||||
return badRequestError(err)
|
||||
}
|
||||
|
||||
if limitRulesPerGroup == 0 && (len(stateFilterSet) > 0 || len(healthFilterSet) > 0) {
|
||||
return badRequestError(errors.New("limit_rules=0 cannot be used together with state or health filters"))
|
||||
}
|
||||
|
||||
var labelOptions []ngmodels.LabelOption
|
||||
if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) {
|
||||
labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels())
|
||||
@@ -1030,9 +1045,12 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
|
||||
}
|
||||
|
||||
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
|
||||
// Only set totals if limit_rules is not 0
|
||||
if limitRulesPerGroup != 0 {
|
||||
ruleGroup.Totals = totals
|
||||
for k, v := range totals {
|
||||
rulesTotals[k] += v
|
||||
}
|
||||
}
|
||||
|
||||
if len(stateFilterSet) > 0 {
|
||||
@@ -1051,15 +1069,15 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
|
||||
ruleGroup.Rules = ruleGroup.Rules[0:limitRulesPerGroup]
|
||||
}
|
||||
|
||||
if len(ruleGroup.Rules) > 0 {
|
||||
if len(ruleGroup.Rules) > 0 || limitRulesPerGroup == 0 {
|
||||
ruleResponse.Data.RuleGroups = append(ruleResponse.Data.RuleGroups, *ruleGroup)
|
||||
}
|
||||
}
|
||||
|
||||
ruleResponse.Data.NextToken = newToken
|
||||
|
||||
// Only return Totals if there is no pagination
|
||||
if maxGroups == -1 {
|
||||
// Only return Totals if there is no pagination and limit_rules is not 0
|
||||
if maxGroups == -1 && limitRulesPerGroup != 0 {
|
||||
ruleResponse.Data.Totals = rulesTotals
|
||||
}
|
||||
|
||||
|
||||
@@ -435,7 +435,8 @@ type GetGrafanaRuleStatusesParams struct {
|
||||
// default: -1
|
||||
LimitAlerts int64 `json:"limit_alerts"`
|
||||
|
||||
// Limit the number of rules per group.
|
||||
// Limit the number of rules per group. When set to 0, returns empty rule groups (only group metadata without rules).
|
||||
// limit_rules=0 cannot be used together with state or health filters, and returns an empty response.
|
||||
// in: query
|
||||
// required: false
|
||||
// default: -1
|
||||
|
||||
@@ -7525,7 +7525,7 @@
|
||||
},
|
||||
{
|
||||
"default": -1,
|
||||
"description": "Limit the number of rules per group.",
|
||||
"description": "Limit the number of rules per group. When set to 0, returns empty rule groups (only group metadata without rules).\nlimit_rules=0 cannot be used together with state or health filters, and returns an empty response.",
|
||||
"format": "int64",
|
||||
"in": "query",
|
||||
"name": "limit_rules",
|
||||
|
||||
@@ -1913,7 +1913,7 @@
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"default": -1,
|
||||
"description": "Limit the number of rules per group.",
|
||||
"description": "Limit the number of rules per group. When set to 0, returns empty rule groups (only group metadata without rules).\nlimit_rules=0 cannot be used together with state or health filters, and returns an empty response.",
|
||||
"name": "limit_rules",
|
||||
"in": "query"
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ export type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource' | 'nam
|
||||
datasources?: string[];
|
||||
panelId?: number;
|
||||
limitAlerts?: number;
|
||||
limitRules?: number;
|
||||
ruleLimit?: number;
|
||||
contactPoint?: string;
|
||||
health?: RuleHealth[];
|
||||
@@ -98,6 +99,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
groupLimit,
|
||||
ruleLimit,
|
||||
limitAlerts,
|
||||
limitRules,
|
||||
groupNextToken,
|
||||
title,
|
||||
datasources,
|
||||
@@ -115,6 +117,7 @@ export const prometheusApi = alertingApi.injectEndpoints({
|
||||
state: state,
|
||||
rule_type: type,
|
||||
limit_alerts: limitAlerts,
|
||||
limit_rules: limitRules?.toFixed(0),
|
||||
rule_limit: ruleLimit?.toFixed(0),
|
||||
group_limit: groupLimit?.toFixed(0),
|
||||
group_next_token: groupNextToken,
|
||||
|
||||
@@ -18,6 +18,12 @@ jest.mock('../../../api/prometheusApi', () => ({
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
featureToggles: {
|
||||
alertingUIUseLimitRules: false,
|
||||
},
|
||||
apps: {},
|
||||
},
|
||||
getDataSourceSrv: () => ({
|
||||
getList: jest.fn().mockReturnValue([]),
|
||||
}),
|
||||
@@ -71,6 +77,7 @@ describe('useNamespaceAndGroupOptions', () => {
|
||||
|
||||
expect(mockFetchGrafanaGroups).toHaveBeenCalledWith({
|
||||
limitAlerts: 0,
|
||||
limitRules: undefined,
|
||||
searchGroupName: 'cpu',
|
||||
groupLimit: 100,
|
||||
});
|
||||
@@ -80,6 +87,27 @@ describe('useNamespaceAndGroupOptions', () => {
|
||||
expect(options[1]).toEqual({ label: 'cpu-usage', value: 'cpu-usage' });
|
||||
});
|
||||
|
||||
it('should pass limitRules: 0 when alertingUIUseLimitRules is enabled', async () => {
|
||||
const { config } = require('@grafana/runtime');
|
||||
config.featureToggles.alertingUIUseLimitRules = true;
|
||||
|
||||
mockFetchGrafanaGroups.mockReturnValue({
|
||||
unwrap: () => Promise.resolve({ data: { groups: [] } }),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useNamespaceAndGroupOptions());
|
||||
await result.current.groupOptions('cpu');
|
||||
|
||||
expect(mockFetchGrafanaGroups).toHaveBeenCalledWith({
|
||||
limitAlerts: 0,
|
||||
limitRules: 0,
|
||||
searchGroupName: 'cpu',
|
||||
groupLimit: 100,
|
||||
});
|
||||
|
||||
config.featureToggles.alertingUIUseLimitRules = false;
|
||||
});
|
||||
|
||||
it('should show message when no groups match search', async () => {
|
||||
mockFetchGrafanaGroups.mockReturnValue({
|
||||
unwrap: () =>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { ComboboxOption } from '@grafana/ui';
|
||||
import { GrafanaPromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
@@ -63,8 +63,10 @@ export function useNamespaceAndGroupOptions(): {
|
||||
const namespaceOptions = useCallback(
|
||||
async (inputValue: string) => {
|
||||
// Grafana namespaces - fetch with limit to check threshold
|
||||
const useLimitRules = config.featureToggles.alertingUIUseLimitRules;
|
||||
const grafanaResponse = await fetchGrafanaGroups({
|
||||
limitAlerts: 0,
|
||||
limitRules: useLimitRules ? 0 : undefined,
|
||||
groupLimit: NAMESPACE_THRESHOLD_LIMIT + 1,
|
||||
}).unwrap();
|
||||
const grafanaFolderNames = Array.from(
|
||||
@@ -135,8 +137,10 @@ export function useNamespaceAndGroupOptions(): {
|
||||
|
||||
try {
|
||||
// Use the backend search with lightweight response
|
||||
const useLimitRules = config.featureToggles.alertingUIUseLimitRules;
|
||||
const grafanaResponse = await fetchGrafanaGroups({
|
||||
limitAlerts: 0, // Lightweight - no alert data
|
||||
limitRules: useLimitRules ? 0 : undefined,
|
||||
searchGroupName: trimmedInput, // Backend filtering via search.rule_group parameter
|
||||
groupLimit: GROUP_SEARCH_LIMIT, // Reasonable limit for dropdown results
|
||||
}).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user