Skip to content

Commit

Permalink
Add resources for creating ML managed alerts
Browse files Browse the repository at this point in the history
  • Loading branch information
csmarchbanks committed Jul 9, 2024
1 parent 6beb516 commit 05b1c06
Show file tree
Hide file tree
Showing 9 changed files with 492 additions and 6 deletions.
44 changes: 44 additions & 0 deletions docs/resources/machine_learning_alert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "grafana_machine_learning_alert Resource - terraform-provider-grafana"
subcategory: "Machine Learning"
description: |-
A job defines the queries and model parameters for a machine learning task.
---

# grafana_machine_learning_alert (Resource)

A job defines the queries and model parameters for a machine learning task.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `title` (String) The title of the alert.

### Optional

- `annotations` (Map of String) Annotations to add to the alert generated in Grafana.
- `anomaly_condition` (String) The condition for when to consider a point as anomalous.
- `for` (String) How long values must be anomalous before firing an alert.
- `job_id` (String) The forecast this alert belongs to.
- `labels` (Map of String) Labels to add to the alert generated in Grafana.
- `no_data_state` (String) How the alert should be processed when no data is returned by the underlying series
- `outlier_id` (String) The forecast this alert belongs to.
- `threshold` (String) The threshold of points over the window that need to be anomalous to alert.
- `window` (String) How much time to average values over

### Read-Only

- `id` (String) The ID of the alert.

## Import

Import is supported using the following syntax:

```shell
terraform import grafana_machine_learning_alert.name "{{ id }}"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import grafana_machine_learning_alert.name "{{ id }}"
36 changes: 36 additions & 0 deletions examples/resources/grafana_machine_learning_alert/job_alert.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
resource "grafana_data_source" "foo" {
type = "prometheus"
name = "prometheus-ds-test"
uid = "prometheus-ds-test-uid"
url = "https://my-instance.com"
basic_auth_enabled = true
basic_auth_username = "username"

json_data_encoded = jsonencode({
httpMethod = "POST"
prometheusType = "Mimir"
prometheusVersion = "2.4.0"
})

secure_json_data_encoded = jsonencode({
basicAuthPassword = "password"
})
}

resource "grafana_machine_learning_job" "test_job" {
name = "Test Job"
metric = "tf_test_job"
datasource_type = "prometheus"
datasource_uid = grafana_data_source.foo.uid
query_params = {
expr = "grafanacloud_grafana_instance_active_user_count"
}
}

resource "grafana_machine_learning_alert" "test_job_alert" {
job_id = grafana_machine_learning_job.test_job.id
title = "Test Alert"
anomaly_condition = "any"
threshold = ">0.8"
window = "15m"
}
26 changes: 26 additions & 0 deletions examples/resources/grafana_machine_learning_alert/outlier_alert.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "grafana_machine_learning_outlier_detector" "my_dbscan_outlier_detector" {
name = "My DBSCAN outlier detector"
description = "My DBSCAN Outlier Detector"

metric = "tf_test_dbscan_job"
datasource_type = "prometheus"
datasource_uid = "AbCd12345"
query_params = {
expr = "grafanacloud_grafana_instance_active_user_count"
}
interval = 300

algorithm {
name = "dbscan"
sensitivity = 0.5
config {
epsilon = 1.0
}
}
}

resource "grafana_machine_learning_alert" "test_job_alert" {
outlier_id = grafana_machine_learning_outlier_detector.my_dbscan_outlier_detector.id
title = "Test Alert"
window = "1h"
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/grafana/amixr-api-go-client v0.0.12 // main branch
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab
github.com/grafana/grafana-openapi-client-go v0.0.0-20240523010106-657d101fcbd9
github.com/grafana/machine-learning-go-client v0.7.0
github.com/grafana/machine-learning-go-client v0.7.1-0.20240709184107-86215ffa4596
github.com/grafana/slo-openapi-client/go v0.0.0-20240626093634-e6741482b090
github.com/grafana/synthetic-monitoring-agent v0.24.3
github.com/grafana/synthetic-monitoring-api-go-client v0.8.0
Expand All @@ -30,6 +30,7 @@ require (
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-mux v0.16.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0
github.com/prometheus/common v0.53.0
github.com/stretchr/testify v1.9.0
github.com/tmccombs/hcl2json v0.6.3
github.com/urfave/cli/v2 v2.27.2
Expand Down Expand Up @@ -129,7 +130,6 @@ require (
github.com/posener/complete v1.2.3 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.14.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4 h1:e7cZfDiNodjQn63be9m8zfnvMEQAMqHVFswjcbdlspk=
github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
github.com/grafana/amixr-api-go-client v0.0.12 h1:oEHZTBhxoZ35EsfeccZBJGPKhZUVOmdSir3WWnSJMLc=
github.com/grafana/amixr-api-go-client v0.0.12/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab h1:/5R8NO996/keDkZqKXEkU3/QgFs1wzChKYkakjsBpRk=
Expand All @@ -144,8 +142,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20240523010106-657d101fcbd9
github.com/grafana/grafana-openapi-client-go v0.0.0-20240523010106-657d101fcbd9/go.mod h1:hiZnMmXc9KXNUlvkV2BKFsiWuIFF/fF4wGgYWEjBitI=
github.com/grafana/grafana-plugin-sdk-go v0.235.0 h1:UnZ/iBDvCkfDgwR94opi8trAWJXv4V8Qr1ocJKRRmqA=
github.com/grafana/grafana-plugin-sdk-go v0.235.0/go.mod h1:6n9LbrjGL3xAATntYVNcIi90G9BVHRJjzHKz5FXVfWw=
github.com/grafana/machine-learning-go-client v0.7.0 h1:yiRBg8rCNbHh9BURa+vtZ8ItRYvabbdYAtsAOfxoFPI=
github.com/grafana/machine-learning-go-client v0.7.0/go.mod h1:bKsLSJTreH7HXaL2FJnnrliMuP0L8XwMkXte6AgwFFg=
github.com/grafana/machine-learning-go-client v0.7.1-0.20240709184107-86215ffa4596 h1:HDL0Y6RiZbal9K+FfprWNjuVnG0RAAmr85pv0POXHNo=
github.com/grafana/machine-learning-go-client v0.7.1-0.20240709184107-86215ffa4596/go.mod h1:9xRIoH6Y6RubuCPNjLfpckE/fLVe9dazg3HSLI1ARAU=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/pyroscope-go/godeltaprof v0.1.7 h1:C11j63y7gymiW8VugJ9ZW0pWfxTZugdSJyC48olk5KY=
Expand Down
222 changes: 222 additions & 0 deletions internal/resources/machinelearning/resource_alert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package machinelearning

import (
"context"

"github.com/grafana/machine-learning-go-client/mlapi"
"github.com/grafana/terraform-provider-grafana/v3/internal/common"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/prometheus/common/model"
)

var resourceAlertID = common.NewResourceID(common.StringIDField("id"))

func resourceAlert() *common.Resource {
schema := &schema.Resource{

Description: `
A job defines the queries and model parameters for a machine learning task.
`,

CreateContext: checkClient(resourceAlertCreate),
ReadContext: checkClient(resourceAlertRead),
UpdateContext: checkClient(resourceAlertUpdate),
DeleteContext: checkClient(resourceAlertDelete),
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"job_id": {
Description: "The forecast this alert belongs to.",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ExactlyOneOf: []string{"job_id", "outlier_id"},
},
"outlier_id": {
Description: "The forecast this alert belongs to.",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ExactlyOneOf: []string{"job_id", "outlier_id"},
},
"id": {
Description: "The ID of the alert.",
Type: schema.TypeString,
Computed: true,
},
"title": {
Description: "The title of the alert.",
Type: schema.TypeString,
Required: true,
},
"anomaly_condition": {
Description: "The condition for when to consider a point as anomalous.",
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"any", "low", "high"}, false),
},
"for": {
Description: "How long values must be anomalous before firing an alert.",
Type: schema.TypeString,
Optional: true,
},
"threshold": {
Description: "The threshold of points over the window that need to be anomalous to alert.",
Type: schema.TypeString,
Optional: true,
},
"window": {
Description: "How much time to average values over",
Type: schema.TypeString,
Optional: true,
},
"labels": {
Description: "Labels to add to the alert generated in Grafana.",
Type: schema.TypeMap,
Optional: true,
},
"annotations": {
Description: "Annotations to add to the alert generated in Grafana.",
Type: schema.TypeMap,
Optional: true,
},
"no_data_state": {
Description: "How the alert should be processed when no data is returned by the underlying series",
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"Alerting", "NoData", "OK"}, false),
},
},
}

return common.NewLegacySDKResource(
common.CategoryMachineLearning,
"grafana_machine_learning_alert",
resourceAlertID,
schema,
)
}

func resourceAlertCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*common.Client).MLAPI
alert, err := makeMLAlert(d)
if err != nil {
return diag.FromErr(err)
}
jobID := d.Get("job_id").(string)
if jobID != "" {
alert, err = c.NewJobAlert(ctx, jobID, alert)
} else {
outlierID := d.Get("outlier_id").(string)
alert, err = c.NewOutlierAlert(ctx, outlierID, alert)
}
if err != nil {
return diag.FromErr(err)
}
d.SetId(alert.ID)
return resourceAlertRead(ctx, d, meta)
}

func resourceAlertRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*common.Client).MLAPI
var (
alert mlapi.Alert
err error
)
jobID := d.Get("job_id").(string)
if jobID != "" {
alert, err = c.JobAlert(ctx, jobID, d.Id())
} else {
outlierID := d.Get("outlier_id").(string)
alert, err = c.OutlierAlert(ctx, outlierID, d.Id())
}

if err, shouldReturn := common.CheckReadError("alert", d, err); shouldReturn {
return err
}

d.Set("title", alert.Title)
d.Set("anomaly_condition", alert.AnomalyCondition)
d.Set("for", alert.For)
d.Set("threshold", alert.Threshold)
d.Set("window", alert.Window)
d.Set("labels", alert.Labels)
d.Set("annotations", alert.Annotations)
d.Set("no_data_state", alert.NoDataState)

return nil
}

func resourceAlertUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*common.Client).MLAPI
alert, err := makeMLAlert(d)
if err != nil {
return diag.FromErr(err)
}
jobID := d.Get("job_id").(string)
if jobID != "" {
_, err = c.UpdateJobAlert(ctx, jobID, alert)
} else {
outlierID := d.Get("outlier_id").(string)
_, err = c.UpdateOutlierAlert(ctx, outlierID, alert)
}

if err != nil {
return diag.FromErr(err)
}
return resourceAlertRead(ctx, d, meta)
}

func resourceAlertDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
c := meta.(*common.Client).MLAPI
jobID := d.Get("job_id").(string)
var err error
if jobID != "" {
err = c.DeleteJobAlert(ctx, jobID, d.Id())
} else {
outlierID := d.Get("outlier_id").(string)
err = c.DeleteOutlierAlert(ctx, outlierID, d.Id())
}
return diag.FromErr(err)
}

func makeMLAlert(d *schema.ResourceData) (mlapi.Alert, error) {
forClause, err := parseDuration(d.Get("for").(string))
if err != nil {
return mlapi.Alert{}, err
}
window, err := parseDuration(d.Get("window").(string))
if err != nil {
return mlapi.Alert{}, err
}
labels := map[string]string{}
for k, v := range d.Get("labels").(map[string]interface{}) {
labels[k] = v.(string)
}
annotations := map[string]string{}
for k, v := range d.Get("annotations").(map[string]interface{}) {
annotations[k] = v.(string)
}
return mlapi.Alert{
ID: d.Id(),
Title: d.Get("title").(string),
AnomalyCondition: mlapi.AnomalyCondition(d.Get("anomaly_condition").(string)),
For: forClause,
Threshold: d.Get("threshold").(string),
Window: window,
Labels: labels,
Annotations: annotations,
NoDataState: mlapi.NoDataState(d.Get("no_data_state").(string)),
}, nil
}

func parseDuration(s string) (model.Duration, error) {
if s == "" {
return 0, nil
}
return model.ParseDuration(s)
}
Loading

0 comments on commit 05b1c06

Please sign in to comment.