Skip to content

Commit

Permalink
Alerting: Add support for recording rules (#1736)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Nov 14, 2024
1 parent 0f9f701 commit 85e393d
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 2 deletions.
12 changes: 11 additions & 1 deletion docs/resources/rule_group.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ Optional:
- `is_paused` (Boolean) Sets whether the alert should be paused or not. Defaults to `false`.
- `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[]`.
- `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`.
- `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))
- `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))
- `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))

Read-Only:

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


<a id="nestedblock--rule--record"></a>
### Nested Schema for `rule.record`

Required:

- `from` (String) The ref id of the query node in the data field to use as the source of the metric.
- `metric` (String) The name of the metric to write to.

## Import

Import is supported using the following syntax:
Expand Down
62 changes: 61 additions & 1 deletion internal/resources/grafana/resource_alerting_rule_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ This resource requires Grafana 9.1.0 or later.
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Description: "Notification settings for the rule. If specified, it overrides the notification policies. Available since Grafana 10.4, requires feature flag 'alertingSimplifiedRouting' enabled.",
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.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"contact_point": {
Expand Down Expand Up @@ -244,6 +244,26 @@ This resource requires Grafana 9.1.0 or later.
},
},
},
"record": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Description: "Settings for a recording rule. Available since Grafana 11.2, requires feature flag 'grafanaManagedRecordingRules' to be enabled.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"metric": {
Type: schema.TypeString,
Required: true,
Description: "The name of the metric to write to.",
},
"from": {
Type: schema.TypeString,
Required: true,
Description: "The ref id of the query node in the data field to use as the source of the metric.",
},
},
},
},
},
},
},
Expand Down Expand Up @@ -477,6 +497,12 @@ func packAlertRule(r *models.ProvisionedAlertRule) (interface{}, error) {
if ns != nil {
json["notification_settings"] = ns
}

record := packRecord(r.Record)
if record != nil {
json["record"] = record
}

return json, nil
}

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

return &rule, nil
Expand Down Expand Up @@ -706,3 +733,36 @@ func unpackNotificationSettings(p interface{}) (*models.AlertRuleNotificationSet
}
return &result, nil
}

func packRecord(r *models.Record) interface{} {
if r == nil {
return nil
}
res := map[string]interface{}{}
if r.Metric != nil {
res["metric"] = *r.Metric
}
if r.From != nil {
res["from"] = *r.From
}
return []interface{}{res}
}

func unpackRecord(p interface{}) *models.Record {
if p == nil {
return nil
}
list, ok := p.([]interface{})
if !ok || len(list) == 0 {
return nil
}
jsonData := list[0].(map[string]interface{})
res := &models.Record{}
if v, ok := jsonData["metric"]; ok && v != nil {
res.Metric = common.Ref(v.(string))
}
if v, ok := jsonData["from"]; ok && v != nil {
res.From = common.Ref(v.(string))
}
return res
}
79 changes: 79 additions & 0 deletions internal/resources/grafana/resource_alerting_rule_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,38 @@ func TestAccAlertRule_NotificationSettings(t *testing.T) {
})
}

func TestAccRecordingRule(t *testing.T) {
testutils.CheckCloudInstanceTestsEnabled(t) // TODO: change to 11.3.1 when available

var group models.AlertRuleGroup
var name = acctest.RandString(10)
var metric = "valid_metric"

resource.ParallelTest(t, resource.TestCase{
ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
CheckDestroy: alertingRuleGroupCheckExists.destroyed(&group, nil),
Steps: []resource.TestStep{
{
Config: testAccRecordingRule(name, metric, "A"),
Check: resource.ComposeTestCheckFunc(
alertingRuleGroupCheckExists.exists("grafana_rule_group.my_rule_group", &group),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "name", name),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.#", "1"),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.name", "My Random Walk Alert"),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.data.0.model", "{\"refId\":\"A\"}"),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.record.0.metric", metric),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.record.0.from", "A"),
// ensure fields are cleared as expected
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.for", "0"),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.condition", ""),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.no_data_state", ""),
resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.exec_err_state", ""),
),
},
},
})
}

func testAccAlertRuleGroupInOrgConfig(name string, interval int, disableProvenance bool) string {
return fmt.Sprintf(`
resource "grafana_organization" "test" {
Expand Down Expand Up @@ -825,3 +857,50 @@ resource "grafana_rule_group" "my_rule_group" {
}
}`, name, gr)
}

func testAccRecordingRule(name string, metric string, refID string) string {
return fmt.Sprintf(`
resource "grafana_folder" "rule_folder" {
title = "%[1]s"
}
resource "grafana_data_source" "testdata_datasource" {
name = "%[1]s"
type = "grafana-testdata-datasource"
url = "http://localhost:3333"
}
resource "grafana_rule_group" "my_rule_group" {
name = "%[1]s"
folder_uid = grafana_folder.rule_folder.uid
interval_seconds = 60
rule {
name = "My Random Walk Alert"
// following should be cleared by Grafana
condition = "A"
no_data_state = "NoData"
exec_err_state = "Alerting"
for = "2m"
// Query the datasource.
data {
ref_id = "A"
relative_time_range {
from = 600
to = 0
}
datasource_uid = grafana_data_source.testdata_datasource.uid
model = jsonencode({
intervalMs = 1000
maxDataPoints = 43200
refId = "A"
})
}
record {
metric = "%[2]s"
from = "%[3]s"
}
}
}`, name, metric, refID)
}

0 comments on commit 85e393d

Please sign in to comment.