Alerting: Fix file import/export of recording rules with target datasource uid (#115663)

Alerting: Fix export of recording rules with target datasource uid
(cherry picked from commit 84120fb210)
This commit is contained in:
Alexander Akhmetov
2026-01-06 14:19:41 +00:00
committed by github-actions[bot]
parent fd375132b2
commit 07eaca234e
13 changed files with 426 additions and 59 deletions
@@ -173,4 +173,49 @@ resource "grafana_rule_group" "rule_group_d3e8424bfbf66bc3" {
from = "condition"
}
}
rule {
name = "recording rule with target"
data {
ref_id = "query"
relative_time_range {
from = 18000
to = 10800
}
datasource_uid = "000000002"
model = "{\"expr\":\"rate(http_requests_total[5m])\",\"hide\":false,\"interval\":\"\",\"intervalMs\":1000,\"legendFormat\":\"\",\"maxDataPoints\":100,\"refId\":\"query\"}"
}
data {
ref_id = "reduced"
relative_time_range {
from = 18000
to = 10800
}
datasource_uid = "__expr__"
model = "{\"expression\":\"query\",\"hide\":false,\"intervalMs\":1000,\"maxDataPoints\":100,\"reducer\":\"mean\",\"refId\":\"reduced\",\"type\":\"reduce\"}"
}
data {
ref_id = "condition"
relative_time_range {
from = 18000
to = 10800
}
datasource_uid = "__expr__"
model = "{\"expression\":\"$reduced > 5\",\"hide\":false,\"intervalMs\":1000,\"maxDataPoints\":100,\"refId\":\"condition\",\"type\":\"math\"}"
}
is_paused = false
record {
metric = "http_requests_rate"
from = "condition"
target_datasource_uid = "000000003"
}
}
}
@@ -233,6 +233,67 @@
"metric": "test_metric",
"from": "condition"
}
},
{
"title": "recording rule with target",
"data": [
{
"refId": "query",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "000000002",
"model": {
"expr": "rate(http_requests_total[5m])",
"hide": false,
"interval": "",
"intervalMs": 1000,
"legendFormat": "",
"maxDataPoints": 100,
"refId": "query"
}
},
{
"refId": "reduced",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "query",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 100,
"reducer": "mean",
"refId": "reduced",
"type": "reduce"
}
},
{
"refId": "condition",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "$reduced \u003e 5",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 100,
"refId": "condition",
"type": "math"
}
}
],
"isPaused": false,
"record": {
"metric": "http_requests_rate",
"from": "condition",
"targetDatasourceUid": "000000003"
}
}
]
}
@@ -187,3 +187,48 @@ groups:
record:
metric: test_metric
from: condition
- title: recording rule with target
data:
- refId: query
relativeTimeRange:
from: 18000
to: 10800
datasourceUid: "000000002"
model:
expr: rate(http_requests_total[5m])
hide: false
interval: ""
intervalMs: 1000
legendFormat: ""
maxDataPoints: 100
refId: query
- refId: reduced
relativeTimeRange:
from: 18000
to: 10800
datasourceUid: __expr__
model:
expression: query
hide: false
intervalMs: 1000
maxDataPoints: 100
reducer: mean
refId: reduced
type: reduce
- refId: condition
relativeTimeRange:
from: 18000
to: 10800
datasourceUid: __expr__
model:
expression: $reduced > 5
hide: false
intervalMs: 1000
maxDataPoints: 100
refId: condition
type: math
isPaused: false
record:
metric: http_requests_rate
from: condition
targetDatasourceUid: "000000003"
@@ -240,6 +240,71 @@
"from": "condition"
}
}
},
{
"grafana_alert": {
"title": "recording rule with target",
"data": [
{
"refId": "query",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "000000002",
"model": {
"expr": "rate(http_requests_total[5m])",
"hide": false,
"interval": "",
"intervalMs": 1000,
"legendFormat": "",
"maxDataPoints": 100,
"refId": "query"
}
},
{
"refId": "reduced",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "query",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 100,
"reducer": "mean",
"refId": "reduced",
"type": "reduce"
}
},
{
"refId": "condition",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "$reduced > 5",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 100,
"refId": "condition",
"type": "math"
}
}
],
"record": {
"metric": "http_requests_rate",
"from": "condition",
"target_datasource_uid": "000000003"
}
}
}
]
}
@@ -19,6 +19,7 @@ const (
testFileDasboardTypoSupport = "./testdata/alert_rules/dasboard-typo-support"
testFileMultipleRules = "./testdata/alert_rules/multiple-rules"
testFileMultipleFiles = "./testdata/alert_rules/multiple-files"
testFileRecordingRules = "./testdata/alert_rules/recording-rules"
testFileCorrectProperties_cp = "./testdata/contact_points/correct-properties"
testFileCorrectPropertiesWithOrg_cp = "./testdata/contact_points/correct-properties-with-org"
testFileEmptyUID = "./testdata/contact_points/empty-uid"
@@ -188,4 +189,43 @@ func TestConfigReader(t *testing.T) {
}
})
})
t.Run("recording rules should parse correctly", func(t *testing.T) {
ruleFiles, err := configReader.readConfig(ctx, testFileRecordingRules)
require.NoError(t, err)
require.Len(t, ruleFiles, 2)
findRule := func(title string) *AlertingFile {
for _, rf := range ruleFiles {
if rf.Groups[0].Title == title {
return rf
}
}
return nil
}
ruleWithTarget := findRule("recording_rules_group")
require.NotNil(t, ruleWithTarget)
require.Len(t, ruleWithTarget.Groups, 1)
require.Len(t, ruleWithTarget.Groups[0].Rules, 1)
ruleWith := ruleWithTarget.Groups[0].Rules[0]
require.NotNil(t, ruleWith.Record)
require.Equal(t, "my_recorded_metric", ruleWith.Record.Metric)
require.Equal(t, "A", ruleWith.Record.From)
require.Equal(t, "mimir-uid", ruleWith.Record.TargetDatasourceUID)
ruleWithoutTarget := findRule("recording_rules_group_no_target")
require.NotNil(t, ruleWithoutTarget)
require.Len(t, ruleWithoutTarget.Groups, 1)
require.Len(t, ruleWithoutTarget.Groups[0].Rules, 1)
ruleWithout := ruleWithoutTarget.Groups[0].Rules[0]
require.NotNil(t, ruleWithout.Record)
require.Equal(t, "http_requests_rate", ruleWithout.Record.Metric)
require.Equal(t, "A", ruleWithout.Record.From)
require.Equal(t, "", ruleWithout.Record.TargetDatasourceUID)
})
}
@@ -303,13 +303,15 @@ func (nsV1 *NotificationSettingsV1) mapToModel() (models.NotificationSettings, e
}
type RecordV1 struct {
Metric values.StringValue `json:"metric" yaml:"metric"`
From values.StringValue `json:"from" yaml:"from"`
Metric values.StringValue `json:"metric" yaml:"metric"`
From values.StringValue `json:"from" yaml:"from"`
TargetDatasourceUID values.StringValue `json:"targetDatasourceUid" yaml:"targetDatasourceUid"`
}
func (record *RecordV1) mapToModel() (models.Record, error) {
return models.Record{
Metric: record.Metric.Value(),
From: record.From.Value(),
Metric: record.Metric.Value(),
From: record.From.Value(),
TargetDatasourceUID: record.TargetDatasourceUID.Value(),
}, nil
}
@@ -208,6 +208,15 @@ func TestRecordingRules(t *testing.T) {
_, err := rule.mapToModel(1)
require.NoError(t, err)
})
t.Run("a valid rule with empty targetDatasourceUid should not error", func(t *testing.T) {
rule := validRecordingRuleV1(t)
rule.Record.TargetDatasourceUID = stringToStringValue("")
model, err := rule.mapToModel(1)
require.NoError(t, err)
require.NotNil(t, model.Record)
require.Equal(t, "", model.Record.TargetDatasourceUID)
})
}
func TestNotificationsSettingsV1MapToModel(t *testing.T) {
@@ -307,80 +316,43 @@ func TestNotificationsSettingsV1MapToModel(t *testing.T) {
func validRuleGroupV1(t *testing.T) AlertRuleGroupV1 {
t.Helper()
var (
orgID values.Int64Value
name values.StringValue
folder values.StringValue
interval values.StringValue
)
var orgID values.Int64Value
err := yaml.Unmarshal([]byte("1"), &orgID)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("Test"), &name)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("Test"), &folder)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("10s"), &interval)
require.NoError(t, err)
return AlertRuleGroupV1{
OrgID: orgID,
Name: name,
Folder: folder,
Interval: interval,
Name: stringToStringValue("Test"),
Folder: stringToStringValue("Test"),
Interval: stringToStringValue("10s"),
Rules: []AlertRuleV1{},
}
}
func validRuleV1(t *testing.T) AlertRuleV1 {
t.Helper()
var (
title values.StringValue
uid values.StringValue
forDuration values.StringValue
condition values.StringValue
)
err := yaml.Unmarshal([]byte("test"), &title)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("test_uid"), &uid)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("10s"), &forDuration)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("A"), &condition)
require.NoError(t, err)
return AlertRuleV1{
Title: title,
UID: uid,
For: forDuration,
Condition: condition,
Title: stringToStringValue("test"),
UID: stringToStringValue("test_uid"),
For: stringToStringValue("10s"),
Condition: stringToStringValue("A"),
Data: []QueryV1{{}},
}
}
func validRecordingRuleV1(t *testing.T) AlertRuleV1 {
t.Helper()
var (
title values.StringValue
uid values.StringValue
forDuration values.StringValue
metric values.StringValue
from values.StringValue
)
err := yaml.Unmarshal([]byte("test"), &title)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("test_uid"), &uid)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("10s"), &forDuration)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("test_metric"), &metric)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("A"), &from)
require.NoError(t, err)
return AlertRuleV1{
Title: title,
UID: uid,
For: forDuration,
Title: stringToStringValue("test"),
UID: stringToStringValue("test_uid"),
For: stringToStringValue("10s"),
Record: &RecordV1{
Metric: metric,
From: from,
Metric: stringToStringValue("test_metric"),
From: stringToStringValue("A"),
TargetDatasourceUID: stringToStringValue("test_target_datasource"),
},
Data: []QueryV1{{}},
}
@@ -0,0 +1,26 @@
apiVersion: 1
groups:
- name: recording_rules_group
folder: my_folder
interval: 1m
rules:
- uid: recording_rule_with_target
title: my_recording_rule_with_target
condition: A
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUid: prometheus-uid
model:
expr: up{instance="localhost:9090"}
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
record:
metric: my_recorded_metric
from: A
targetDatasourceUid: mimir-uid
@@ -0,0 +1,25 @@
apiVersion: 1
groups:
- name: recording_rules_group_no_target
folder: my_folder
interval: 1m
rules:
- uid: recording_rule_without_target
title: my_recording_rule_without_target
condition: A
data:
- refId: A
queryType: ''
relativeTimeRange:
from: 600
to: 0
datasourceUid: prometheus-uid
model:
expr: rate(http_requests_total[5m])
instant: true
intervalMs: 1000
maxDataPoints: 43200
refId: A
record:
metric: http_requests_rate
from: A
@@ -70,6 +70,33 @@
},
"isPaused": false,
"missing_series_evals_to_resolve": 2
},
{
"uid": "<dynamic>",
"title": "RecordingRule1",
"data": [
{
"refId": "A",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "__expr__",
"model": {
"expression": "1 + 1",
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A",
"type": "math"
}
}
],
"isPaused": false,
"record": {
"metric": "test_metric",
"from": "A",
"targetDatasourceUid": "test-datasource-uid"
}
}
]
}
@@ -97,6 +97,44 @@
},
"missing_series_evals_to_resolve": 2
}
},
{
"expr": "",
"for": "0s",
"keep_firing_for": "0s",
"grafana_alert": {
"title": "RecordingRule1",
"data": [
{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "__expr__",
"model": {
"expression": "1 + 1",
"intervalMs": 1000,
"maxDataPoints": 43200,
"type": "math"
}
}
],
"updated": "2023-09-29T17:37:19Z",
"intervalSeconds": 60,
"version": 1,
"uid": "<dynamic>",
"namespace_uid": "<dynamic>",
"rule_group": "Group1",
"is_paused": false,
"record": {
"metric": "test_metric",
"from": "A",
"target_datasource_uid": "test-datasource-uid"
},
"metadata": {}
}
}
]
}
@@ -53,6 +53,26 @@
"exec_err_state": "Alerting",
"missing_series_evals_to_resolve": 2
}
},
{
"grafana_alert": {
"title": "RecordingRule1",
"data": [
{
"refId": "A",
"datasourceUid": "__expr__",
"model": {
"expression": "1 + 1",
"type": "math"
}
}
],
"record": {
"metric": "test_metric",
"from": "A",
"target_datasource_uid": "test-datasource-uid"
}
}
}
]
}
+1
View File
@@ -249,6 +249,7 @@ func convertGettableGrafanaRuleToPostable(gettable *apimodels.GettableGrafanaRul
ExecErrState: gettable.ExecErrState,
IsPaused: &gettable.IsPaused,
NotificationSettings: gettable.NotificationSettings,
Record: gettable.Record,
Metadata: gettable.Metadata,
}
}