Skip to content

Commit 85e393d

Browse files
rwwivjulienduchesnespinillos
authored
Alerting: Add support for recording rules (#1736)
* Add support for recording rules * Update docs * Update test to cloud-only * Ensure fields are cleared as expected * Add type assertion safety check Co-authored-by: Selene <[email protected]> --------- Co-authored-by: Julien Duchesne <[email protected]> Co-authored-by: Selene <[email protected]>
1 parent 0f9f701 commit 85e393d

File tree

3 files changed

+151
-2
lines changed

3 files changed

+151
-2
lines changed

docs/resources/rule_group.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ Optional:
144144
- `is_paused` (Boolean) Sets whether the alert should be paused or not. Defaults to `false`.
145145
- `labels` (Map of String) Key-value pairs to attach to the alert rule that can be used in matching, grouping, and routing. Defaults to `map[]`.
146146
- `no_data_state` (String) Describes what state to enter when the rule's query returns No Data. Options are OK, NoData, KeepLast, and Alerting. Defaults to `NoData`.
147-
- `notification_settings` (Block List, Max: 1) Notification settings for the rule. If specified, it overrides the notification policies. Available since Grafana 10.4, requires feature flag 'alertingSimplifiedRouting' enabled. (see [below for nested schema](#nestedblock--rule--notification_settings))
147+
- `notification_settings` (Block List, Max: 1) Notification settings for the rule. If specified, it overrides the notification policies. Available since Grafana 10.4, requires feature flag 'alertingSimplifiedRouting' to be enabled. (see [below for nested schema](#nestedblock--rule--notification_settings))
148+
- `record` (Block List, Max: 1) Settings for a recording rule. Available since Grafana 11.2, requires feature flag 'grafanaManagedRecordingRules' to be enabled. (see [below for nested schema](#nestedblock--rule--record))
148149

149150
Read-Only:
150151

@@ -189,6 +190,15 @@ Optional:
189190
- `mute_timings` (List of String) A list of mute timing names to apply to alerts that match this policy.
190191
- `repeat_interval` (String) Minimum time interval for re-sending a notification if an alert is still firing. Default is 4 hours.
191192

193+
194+
<a id="nestedblock--rule--record"></a>
195+
### Nested Schema for `rule.record`
196+
197+
Required:
198+
199+
- `from` (String) The ref id of the query node in the data field to use as the source of the metric.
200+
- `metric` (String) The name of the metric to write to.
201+
192202
## Import
193203

194204
Import is supported using the following syntax:

internal/resources/grafana/resource_alerting_rule_group.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ This resource requires Grafana 9.1.0 or later.
202202
Type: schema.TypeList,
203203
MaxItems: 1,
204204
Optional: true,
205-
Description: "Notification settings for the rule. If specified, it overrides the notification policies. Available since Grafana 10.4, requires feature flag 'alertingSimplifiedRouting' enabled.",
205+
Description: "Notification settings for the rule. If specified, it overrides the notification policies. Available since Grafana 10.4, requires feature flag 'alertingSimplifiedRouting' to be enabled.",
206206
Elem: &schema.Resource{
207207
Schema: map[string]*schema.Schema{
208208
"contact_point": {
@@ -244,6 +244,26 @@ This resource requires Grafana 9.1.0 or later.
244244
},
245245
},
246246
},
247+
"record": {
248+
Type: schema.TypeList,
249+
MaxItems: 1,
250+
Optional: true,
251+
Description: "Settings for a recording rule. Available since Grafana 11.2, requires feature flag 'grafanaManagedRecordingRules' to be enabled.",
252+
Elem: &schema.Resource{
253+
Schema: map[string]*schema.Schema{
254+
"metric": {
255+
Type: schema.TypeString,
256+
Required: true,
257+
Description: "The name of the metric to write to.",
258+
},
259+
"from": {
260+
Type: schema.TypeString,
261+
Required: true,
262+
Description: "The ref id of the query node in the data field to use as the source of the metric.",
263+
},
264+
},
265+
},
266+
},
247267
},
248268
},
249269
},
@@ -477,6 +497,12 @@ func packAlertRule(r *models.ProvisionedAlertRule) (interface{}, error) {
477497
if ns != nil {
478498
json["notification_settings"] = ns
479499
}
500+
501+
record := packRecord(r.Record)
502+
if record != nil {
503+
json["record"] = record
504+
}
505+
480506
return json, nil
481507
}
482508

@@ -516,6 +542,7 @@ func unpackAlertRule(raw interface{}, groupName string, folderUID string, orgID
516542
Annotations: unpackMap(json["annotations"]),
517543
IsPaused: json["is_paused"].(bool),
518544
NotificationSettings: ns,
545+
Record: unpackRecord(json["record"]),
519546
}
520547

521548
return &rule, nil
@@ -706,3 +733,36 @@ func unpackNotificationSettings(p interface{}) (*models.AlertRuleNotificationSet
706733
}
707734
return &result, nil
708735
}
736+
737+
func packRecord(r *models.Record) interface{} {
738+
if r == nil {
739+
return nil
740+
}
741+
res := map[string]interface{}{}
742+
if r.Metric != nil {
743+
res["metric"] = *r.Metric
744+
}
745+
if r.From != nil {
746+
res["from"] = *r.From
747+
}
748+
return []interface{}{res}
749+
}
750+
751+
func unpackRecord(p interface{}) *models.Record {
752+
if p == nil {
753+
return nil
754+
}
755+
list, ok := p.([]interface{})
756+
if !ok || len(list) == 0 {
757+
return nil
758+
}
759+
jsonData := list[0].(map[string]interface{})
760+
res := &models.Record{}
761+
if v, ok := jsonData["metric"]; ok && v != nil {
762+
res.Metric = common.Ref(v.(string))
763+
}
764+
if v, ok := jsonData["from"]; ok && v != nil {
765+
res.From = common.Ref(v.(string))
766+
}
767+
return res
768+
}

internal/resources/grafana/resource_alerting_rule_group_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,38 @@ func TestAccAlertRule_NotificationSettings(t *testing.T) {
675675
})
676676
}
677677

678+
func TestAccRecordingRule(t *testing.T) {
679+
testutils.CheckCloudInstanceTestsEnabled(t) // TODO: change to 11.3.1 when available
680+
681+
var group models.AlertRuleGroup
682+
var name = acctest.RandString(10)
683+
var metric = "valid_metric"
684+
685+
resource.ParallelTest(t, resource.TestCase{
686+
ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
687+
CheckDestroy: alertingRuleGroupCheckExists.destroyed(&group, nil),
688+
Steps: []resource.TestStep{
689+
{
690+
Config: testAccRecordingRule(name, metric, "A"),
691+
Check: resource.ComposeTestCheckFunc(
692+
alertingRuleGroupCheckExists.exists("grafana_rule_group.my_rule_group", &group),
693+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "name", name),
694+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.#", "1"),
695+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.name", "My Random Walk Alert"),
696+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.data.0.model", "{\"refId\":\"A\"}"),
697+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.record.0.metric", metric),
698+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.record.0.from", "A"),
699+
// ensure fields are cleared as expected
700+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.for", "0"),
701+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.condition", ""),
702+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.no_data_state", ""),
703+
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.exec_err_state", ""),
704+
),
705+
},
706+
},
707+
})
708+
}
709+
678710
func testAccAlertRuleGroupInOrgConfig(name string, interval int, disableProvenance bool) string {
679711
return fmt.Sprintf(`
680712
resource "grafana_organization" "test" {
@@ -825,3 +857,50 @@ resource "grafana_rule_group" "my_rule_group" {
825857
}
826858
}`, name, gr)
827859
}
860+
861+
func testAccRecordingRule(name string, metric string, refID string) string {
862+
return fmt.Sprintf(`
863+
resource "grafana_folder" "rule_folder" {
864+
title = "%[1]s"
865+
}
866+
867+
resource "grafana_data_source" "testdata_datasource" {
868+
name = "%[1]s"
869+
type = "grafana-testdata-datasource"
870+
url = "http://localhost:3333"
871+
}
872+
873+
resource "grafana_rule_group" "my_rule_group" {
874+
name = "%[1]s"
875+
folder_uid = grafana_folder.rule_folder.uid
876+
interval_seconds = 60
877+
878+
rule {
879+
name = "My Random Walk Alert"
880+
// following should be cleared by Grafana
881+
condition = "A"
882+
no_data_state = "NoData"
883+
exec_err_state = "Alerting"
884+
for = "2m"
885+
886+
// Query the datasource.
887+
data {
888+
ref_id = "A"
889+
relative_time_range {
890+
from = 600
891+
to = 0
892+
}
893+
datasource_uid = grafana_data_source.testdata_datasource.uid
894+
model = jsonencode({
895+
intervalMs = 1000
896+
maxDataPoints = 43200
897+
refId = "A"
898+
})
899+
}
900+
record {
901+
metric = "%[2]s"
902+
from = "%[3]s"
903+
}
904+
}
905+
}`, name, metric, refID)
906+
}

0 commit comments

Comments
 (0)