diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 3e6de71e8d3..e8fa818a250 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -73,6 +73,8 @@ export interface UnifiedAlertingConfig { // will be undefined if implementation is not "multiple" alertStateHistoryPrimary?: string; recordingRulesEnabled?: boolean; + // will be undefined if no default datasource is configured + defaultRecordingRulesTargetDatasourceUID?: string; } /** Supported OAuth services diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 33f325f9f42..c19bae362d5 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -159,6 +159,7 @@ export class GrafanaBootConfig implements GrafanaConfig { alertStateHistoryBackend: undefined, alertStateHistoryPrimary: undefined, recordingRulesEnabled: false, + defaultRecordingRulesTargetDatasourceUID: undefined, }; applicationInsightsConnectionString?: string; applicationInsightsEndpointUrl?: string; diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 16ee4d2ac3d..b3e402b0f62 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -94,10 +94,11 @@ type FrontendSettingsAnalyticsDTO struct { } type FrontendSettingsUnifiedAlertingDTO struct { - MinInterval string `json:"minInterval"` - AlertStateHistoryBackend string `json:"alertStateHistoryBackend,omitempty"` - AlertStateHistoryPrimary string `json:"alertStateHistoryPrimary,omitempty"` - RecordingRulesEnabled bool `json:"recordingRulesEnabled"` + MinInterval string `json:"minInterval"` + AlertStateHistoryBackend string `json:"alertStateHistoryBackend,omitempty"` + AlertStateHistoryPrimary string `json:"alertStateHistoryPrimary,omitempty"` + RecordingRulesEnabled bool `json:"recordingRulesEnabled"` + DefaultRecordingRulesTargetDatasourceUID string `json:"defaultRecordingRulesTargetDatasourceUID,omitempty"` } // Enterprise-only diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index fdbbcd8e568..5ef55403d4d 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -351,6 +351,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro } frontendSettings.UnifiedAlerting.RecordingRulesEnabled = hs.Cfg.UnifiedAlerting.RecordingRules.Enabled + frontendSettings.UnifiedAlerting.DefaultRecordingRulesTargetDatasourceUID = hs.Cfg.UnifiedAlerting.RecordingRules.DefaultDatasourceUID if hs.Cfg.UnifiedAlerting.Enabled != nil { frontendSettings.UnifiedAlertingEnabled = *hs.Cfg.UnifiedAlerting.Enabled diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts index 0a2f434663e..3fe9e3d76cc 100644 --- a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts @@ -1,3 +1,6 @@ +import { GrafanaConfig } from '@grafana/data'; +import { config } from '@grafana/runtime'; + import { mockAlertQuery, mockDataSource, mockReduceExpression, mockThresholdExpression } from '../mocks'; import { testWithFeatureToggles } from '../test/test-utils'; import { RuleFormType } from '../types/rule-form'; @@ -191,3 +194,27 @@ describe('getDefaultManualRouting', () => { expect(getDefautManualRouting()).toBe(true); }); }); + +describe('getDefaultFormValues', () => { + // This is for Typescript. GrafanaBootConfig returns narrower types than GrafanaConfig + const grafanaConfig: GrafanaConfig = config; + const uaConfig = grafanaConfig.unifiedAlerting; + + afterEach(() => { + uaConfig.defaultRecordingRulesTargetDatasourceUID = undefined; + }); + + it('should set targetDatasourceUid from config when defaultRecordingRulesTargetDatasourceUID is provided', () => { + const expectedDatasourceUid = 'test-datasource-uid'; + uaConfig.defaultRecordingRulesTargetDatasourceUID = expectedDatasourceUid; + + const result = getDefaultFormValues(); + + expect(result.targetDatasourceUid).toBe(expectedDatasourceUid); + }); + + it('should set targetDatasourceUid to undefined when defaultRecordingRulesTargetDatasourceUID is not provided', () => { + const result = getDefaultFormValues(); + expect(result.targetDatasourceUid).toBeUndefined(); + }); +}); diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.ts index 3c72744397a..b7b9c8c48b0 100644 --- a/public/app/features/alerting/unified/rule-editor/formDefaults.ts +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.ts @@ -71,6 +71,7 @@ export const getDefaultFormValues = (): RuleFormValues => { overrideTimings: false, muteTimeIntervals: [], editorSettings: getDefaultEditorSettings(), + targetDatasourceUid: config.unifiedAlerting?.defaultRecordingRulesTargetDatasourceUID, // cortex / loki namespace: '', diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index 92e5082bd88..7137066229e 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,4 +1,5 @@ import { PromQuery } from '@grafana/prometheus'; +import { RuleWithLocation } from 'app/types/unified-alerting'; import { AlertDataQuery, AlertQuery, @@ -7,7 +8,7 @@ import { RulerAlertingRuleDTO, } from 'app/types/unified-alerting-dto'; -import { mockDataSource } from '../mocks'; +import { mockDataSource, mockRuleWithLocation, mockRulerGrafanaRecordingRule } from '../mocks'; import { getDefaultFormValues } from '../rule-editor/formDefaults'; import { setupDataSources } from '../testSetup/datasources'; import { AlertManagerManualRouting, RuleFormType, RuleFormValues } from '../types/rule-form'; @@ -22,6 +23,7 @@ import { getContactPointsFromDTO, getInstantFromDataQuery, getNotificationSettingsForDTO, + rulerRuleToFormValues, } from './rule-form'; describe('formValuesToRulerGrafanaRuleDTO', () => { @@ -111,6 +113,85 @@ describe('formValuesToRulerGrafanaRuleDTO', () => { expect(alertingRulerRuleToRuleForm(rule)).toMatchSnapshot(); }); }); + +describe('rulerRuleToFormValues', () => { + it('should convert grafana recording rule to form values', () => { + const mockRecordingRule = mockRulerGrafanaRecordingRule({ + grafana_alert: { + uid: 'recording-rule-uid', + title: 'My Recording Rule', + namespace_uid: 'folder-uid', + rule_group: 'recording-group', + condition: 'A', + record: { + metric: 'my_metric', + from: 'A', + target_datasource_uid: 'target-ds-uid', + }, + data: [ + { + datasourceUid: 'prom-uid', + refId: 'A', + queryType: '', + model: { refId: 'A' }, + }, + ], + is_paused: false, + }, + annotations: { + description: 'This is a recording rule', + summary: 'Recording rule summary', + }, + labels: { + team: 'platform', + env: 'production', + }, + }); + + const ruleWithLocation: RuleWithLocation = mockRuleWithLocation(mockRecordingRule, { + ruleSourceName: GRAFANA_RULES_SOURCE_NAME, + namespace: 'Test Folder', + group: { + name: 'recording-group', + interval: '1m', + rules: [mockRecordingRule], + }, + }); + + const result = rulerRuleToFormValues(ruleWithLocation); + + expect(result).toMatchObject({ + name: 'My Recording Rule', + type: RuleFormType.grafanaRecording, + group: 'recording-group', + evaluateEvery: '1m', + queries: [ + { + datasourceUid: 'prom-uid', + refId: 'A', + queryType: '', + model: { refId: 'A' }, + }, + ], + condition: 'A', + annotations: [ + { key: 'summary', value: 'Recording rule summary' }, + { key: 'description', value: 'This is a recording rule' }, + { key: 'runbook_url', value: '' }, + ], + labels: [ + { key: 'team', value: 'platform' }, + { key: 'env', value: 'production' }, + { key: '', value: '' }, // empty row added for form editing + ], + folder: { title: 'Test Folder', uid: 'folder-uid' }, + isPaused: false, + metric: 'my_metric', + targetDatasourceUid: 'target-ds-uid', + }); + }); +}); + describe('getContactPointsFromDTO', () => { it('should return undefined if notification_settings is not defined', () => { const ga: GrafanaRuleDefinition = { diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index b1a77068990..c4208d61f52 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -297,7 +297,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, metric: ga.record?.metric, - targetDatasourceUid: ga.record?.target_datasource_uid, + targetDatasourceUid: ga.record?.target_datasource_uid || defaultFormValues.targetDatasourceUid, }; } else if (rulerRuleType.grafana.rule(rule)) { // grafana alerting rule