Alerting: Use default_datasource_uid as the default target for recording rules in UI (#106415)

* Alerting: Use default_datasource_uid as the default target for recording rules

* Add tests

---------

Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
Alexander Akhmetov
2025-06-10 11:58:42 +02:00
committed by GitHub
co-authored by Konrad Lalik
parent a29e24c5b4
commit 7c3f7b9e8b
8 changed files with 120 additions and 6 deletions
@@ -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
+1
View File
@@ -159,6 +159,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
alertStateHistoryBackend: undefined,
alertStateHistoryPrimary: undefined,
recordingRulesEnabled: false,
defaultRecordingRulesTargetDatasourceUID: undefined,
};
applicationInsightsConnectionString?: string;
applicationInsightsEndpointUrl?: string;
+5 -4
View File
@@ -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
+1
View File
@@ -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
@@ -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();
});
});
@@ -71,6 +71,7 @@ export const getDefaultFormValues = (): RuleFormValues => {
overrideTimings: false,
muteTimeIntervals: [],
editorSettings: getDefaultEditorSettings(),
targetDatasourceUid: config.unifiedAlerting?.defaultRecordingRulesTargetDatasourceUID,
// cortex / loki
namespace: '',
@@ -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 = {
@@ -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