Skip to content

Commit 7e759ec

Browse files
authored
Add support for Machine Learning Outlier Detector (#801)
* Add support for Machine Learning Outlier Detector * Review feedback addressed, cleanups & using TF schema features * Restore tests
1 parent 3bc1bb2 commit 7e759ec

File tree

9 files changed

+561
-5
lines changed

9 files changed

+561
-5
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_machine_learning_outlier_detector Resource - terraform-provider-grafana"
4+
subcategory: "Cloud"
5+
description: |-
6+
An outlier detector monitors the results of a query and reports when its values are outside normal bands.
7+
The normal band is configured by choice of algorithm, its sensitivity and other configuration.
8+
Visit https://grafana.com/docs/grafana-cloud/machine-learning/outlier-detection/ for more details.
9+
---
10+
11+
# grafana_machine_learning_outlier_detector (Resource)
12+
13+
An outlier detector monitors the results of a query and reports when its values are outside normal bands.
14+
15+
The normal band is configured by choice of algorithm, its sensitivity and other configuration.
16+
17+
Visit https://grafana.com/docs/grafana-cloud/machine-learning/outlier-detection/ for more details.
18+
19+
20+
21+
<!-- schema generated by tfplugindocs -->
22+
## Schema
23+
24+
### Required
25+
26+
- `algorithm` (Block Set, Min: 1, Max: 1) The algorithm to use and its configuration. See https://grafana.com/docs/grafana-cloud/machine-learning/outlier-detection/ for details. (see [below for nested schema](#nestedblock--algorithm))
27+
- `datasource_type` (String) The type of datasource being queried. Currently allowed values are prometheus, graphite, loki, postgres, and datadog.
28+
- `metric` (String) The metric used to query the outlier detector results.
29+
- `name` (String) The name of the outlier detector.
30+
- `query_params` (Map of String) An object representing the query params to query Grafana with.
31+
32+
### Optional
33+
34+
- `datasource_id` (Number) The id of the datasource to query.
35+
- `datasource_uid` (String) The uid of the datasource to query.
36+
- `description` (String) A description of the outlier detector.
37+
- `interval` (Number) The data interval in seconds to monitor. Defaults to `300`.
38+
39+
### Read-Only
40+
41+
- `id` (String) The ID of the outlier detector.
42+
43+
<a id="nestedblock--algorithm"></a>
44+
### Nested Schema for `algorithm`
45+
46+
Required:
47+
48+
- `name` (String) The name of the algorithm to use ('mad' or 'dbscan').
49+
- `sensitivity` (Number) Specify the sensitivity of the detector (in range [0,1]).
50+
51+
Optional:
52+
53+
- `config` (Block Set, Max: 1) For DBSCAN only, specify the configuration map (see [below for nested schema](#nestedblock--algorithm--config))
54+
55+
<a id="nestedblock--algorithm--config"></a>
56+
### Nested Schema for `algorithm.config`
57+
58+
Required:
59+
60+
- `epsilon` (Number) Specify the epsilon parameter (positive float)
61+
62+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
resource "grafana_machine_learning_outlier_detector" "my_dbscan_outlier_detector" {
2+
name = "My DBSCAN outlier detector"
3+
description = "My DBSCAN Outlier Detector"
4+
5+
metric = "tf_test_dbscan_job"
6+
datasource_type = "prometheus"
7+
datasource_id = 12
8+
query_params = {
9+
expr = "grafanacloud_grafana_instance_active_user_count"
10+
}
11+
interval = 300
12+
13+
algorithm {
14+
name = "dbscan"
15+
sensitivity = 0.5
16+
config {
17+
epsilon = 1.0
18+
}
19+
}
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
resource "grafana_machine_learning_outlier_detector" "my_mad_outlier_detector" {
2+
name = "My MAD outlier detector"
3+
description = "My MAD Outlier Detector"
4+
5+
metric = "tf_test_mad_job"
6+
datasource_type = "prometheus"
7+
datasource_uid = "AbCd12345"
8+
query_params = {
9+
expr = "grafanacloud_grafana_instance_active_user_count"
10+
}
11+
interval = 300
12+
13+
algorithm {
14+
name = "mad"
15+
sensitivity = 0.7
16+
}
17+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/Masterminds/semver/v3 v3.2.0
77
github.com/grafana/amixr-api-go-client v0.0.6
88
github.com/grafana/grafana-api-golang-client v0.18.2
9-
github.com/grafana/machine-learning-go-client v0.3.0
9+
github.com/grafana/machine-learning-go-client v0.4.0
1010
github.com/grafana/synthetic-monitoring-agent v0.14.1
1111
github.com/grafana/synthetic-monitoring-api-go-client v0.7.0
1212
github.com/hashicorp/go-cleanhttp v0.5.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ github.com/grafana/amixr-api-go-client v0.0.6 h1:dDKSROgxg2NUuNBeF3L/0cfD9nG815O
7979
github.com/grafana/amixr-api-go-client v0.0.6/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
8080
github.com/grafana/grafana-api-golang-client v0.18.2 h1:WPYT4Cyw0uqBHAyO619HykzNsQ98yHKFmPuJonfiW8c=
8181
github.com/grafana/grafana-api-golang-client v0.18.2/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
82-
github.com/grafana/machine-learning-go-client v0.3.0 h1:QmDPt9kFvw7RsVZE92V4tSbng2dHsOsVsHvNczLpNy8=
83-
github.com/grafana/machine-learning-go-client v0.3.0/go.mod h1:QFfZz8NkqVF8++skjkKQXJEZfpCYd8S0yTWJUpsLLTA=
82+
github.com/grafana/machine-learning-go-client v0.4.0 h1:UAkJPE7xujzFTm0d9ctbX/FsCID8rqejWjnkRPGNM6E=
83+
github.com/grafana/machine-learning-go-client v0.4.0/go.mod h1:QFfZz8NkqVF8++skjkKQXJEZfpCYd8S0yTWJUpsLLTA=
8484
github.com/grafana/synthetic-monitoring-agent v0.14.1 h1:UyTCDTFr2gIJujJrspYR9MHGptdNQrbTM5Td36nDinA=
8585
github.com/grafana/synthetic-monitoring-agent v0.14.1/go.mod h1:YfVC6GZykaU8Di3291J8iBOrjA3R2BWiuRrAjlHJLPQ=
8686
github.com/grafana/synthetic-monitoring-api-go-client v0.7.0 h1:3ZfQzmXDBPcQTTgMAIIiTw5Dwxm/B4lzf34Sto0d0YY=

grafana/provider.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ func Provider(version string) func() *schema.Provider {
7878
"grafana_user": ResourceUser(),
7979

8080
// Machine Learning
81-
"grafana_machine_learning_job": ResourceMachineLearningJob(),
82-
"grafana_machine_learning_holiday": ResourceMachineLearningHoliday(),
81+
"grafana_machine_learning_job": ResourceMachineLearningJob(),
82+
"grafana_machine_learning_holiday": ResourceMachineLearningHoliday(),
83+
"grafana_machine_learning_outlier_detector": ResourceMachineLearningOutlierDetector(),
8384
})
8485

8586
// Resources that require the Synthetic Monitoring client to exist.
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/grafana/machine-learning-go-client/mlapi"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
12+
)
13+
14+
func ResourceMachineLearningOutlierDetector() *schema.Resource {
15+
return &schema.Resource{
16+
17+
Description: `
18+
An outlier detector monitors the results of a query and reports when its values are outside normal bands.
19+
20+
The normal band is configured by choice of algorithm, its sensitivity and other configuration.
21+
22+
Visit https://grafana.com/docs/grafana-cloud/machine-learning/outlier-detection/ for more details.
23+
`,
24+
25+
CreateContext: resourceMachineLearningOutlierCreate,
26+
ReadContext: resourceMachineLearningOutlierRead,
27+
UpdateContext: resourceMachineLearningOutlierUpdate,
28+
DeleteContext: resourceMachineLearningOutlierDelete,
29+
Importer: &schema.ResourceImporter{
30+
StateContext: schema.ImportStatePassthroughContext,
31+
},
32+
33+
Schema: map[string]*schema.Schema{
34+
"id": {
35+
Description: "The ID of the outlier detector.",
36+
Type: schema.TypeString,
37+
Computed: true,
38+
},
39+
"name": {
40+
Description: "The name of the outlier detector.",
41+
Type: schema.TypeString,
42+
Required: true,
43+
},
44+
"metric": {
45+
Description: "The metric used to query the outlier detector results.",
46+
Type: schema.TypeString,
47+
Required: true,
48+
},
49+
"description": {
50+
Description: "A description of the outlier detector.",
51+
Type: schema.TypeString,
52+
Optional: true,
53+
},
54+
"datasource_id": {
55+
Description: "The id of the datasource to query.",
56+
Type: schema.TypeInt,
57+
Optional: true,
58+
ExactlyOneOf: []string{"datasource_uid"},
59+
},
60+
"datasource_uid": {
61+
Description: "The uid of the datasource to query.",
62+
Type: schema.TypeString,
63+
Optional: true,
64+
},
65+
"datasource_type": {
66+
Description: "The type of datasource being queried. Currently allowed values are prometheus, graphite, loki, postgres, and datadog.",
67+
Type: schema.TypeString,
68+
Required: true,
69+
ValidateFunc: validation.StringInSlice([]string{"prometheus", "graphite", "loki", "postgres", "datadog"}, false),
70+
},
71+
"query_params": {
72+
Description: "An object representing the query params to query Grafana with.",
73+
Type: schema.TypeMap,
74+
Required: true,
75+
},
76+
"interval": {
77+
Description: "The data interval in seconds to monitor.",
78+
Type: schema.TypeInt,
79+
Optional: true,
80+
Default: 300,
81+
},
82+
"algorithm": {
83+
Description: "The algorithm to use and its configuration. See https://grafana.com/docs/grafana-cloud/machine-learning/outlier-detection/ for details.",
84+
Type: schema.TypeSet,
85+
Required: true,
86+
MaxItems: 1,
87+
Elem: &schema.Resource{
88+
Schema: map[string]*schema.Schema{
89+
"name": {
90+
Description: "The name of the algorithm to use ('mad' or 'dbscan').",
91+
Type: schema.TypeString,
92+
Required: true,
93+
ValidateFunc: validation.StringInSlice([]string{"mad", "dbscan"}, false),
94+
},
95+
"sensitivity": {
96+
Description: "Specify the sensitivity of the detector (in range [0,1]).",
97+
Type: schema.TypeFloat,
98+
Required: true,
99+
ValidateFunc: validation.FloatBetween(0, 1.0),
100+
},
101+
"config": {
102+
Description: "For DBSCAN only, specify the configuration map",
103+
Type: schema.TypeSet,
104+
Optional: true,
105+
MaxItems: 1,
106+
Elem: &schema.Resource{
107+
Schema: map[string]*schema.Schema{
108+
"epsilon": {
109+
Description: "Specify the epsilon parameter (positive float)",
110+
Type: schema.TypeFloat,
111+
Required: true,
112+
ValidateFunc: validation.FloatAtLeast(0),
113+
},
114+
},
115+
},
116+
},
117+
},
118+
},
119+
},
120+
},
121+
}
122+
}
123+
124+
func resourceMachineLearningOutlierCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
125+
c := meta.(*client).mlapi
126+
outlier, err := makeMLOutlier(d, meta)
127+
if err != nil {
128+
return diag.FromErr(err)
129+
}
130+
outlier, err = c.NewOutlierDetector(ctx, outlier)
131+
if err != nil {
132+
return diag.FromErr(err)
133+
}
134+
d.SetId(outlier.ID)
135+
return resourceMachineLearningOutlierRead(ctx, d, meta)
136+
}
137+
138+
func resourceMachineLearningOutlierRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
139+
c := meta.(*client).mlapi
140+
outlier, err := c.OutlierDetector(ctx, d.Id())
141+
if err != nil {
142+
var diags diag.Diagnostics
143+
if strings.HasPrefix(err.Error(), "status: 404") {
144+
name := d.Get("name").(string)
145+
diags = append(diags, diag.Diagnostic{
146+
Severity: diag.Warning,
147+
Summary: fmt.Sprintf("Outlier Detector %q is in Terraform state, but no longer exists in Grafana ML", name),
148+
Detail: fmt.Sprintf("%q will be recreated when you apply", name),
149+
})
150+
d.SetId("")
151+
return diags
152+
}
153+
return diag.FromErr(err)
154+
}
155+
156+
d.Set("name", outlier.Name)
157+
d.Set("metric", outlier.Metric)
158+
d.Set("description", outlier.Description)
159+
if outlier.DatasourceID != 0 {
160+
d.Set("datasource_id", outlier.DatasourceID)
161+
} else {
162+
d.Set("datasource_id", nil)
163+
}
164+
if outlier.DatasourceUID != "" {
165+
d.Set("datasource_uid", outlier.DatasourceUID)
166+
} else {
167+
d.Set("datasource_uid", nil)
168+
}
169+
d.Set("datasource_type", outlier.DatasourceType)
170+
d.Set("query_params", outlier.QueryParams)
171+
d.Set("interval", outlier.Interval)
172+
d.Set("algorithm", convertToSetStructure(outlier.Algorithm))
173+
174+
return nil
175+
}
176+
177+
func resourceMachineLearningOutlierUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
178+
c := meta.(*client).mlapi
179+
outlier, err := makeMLOutlier(d, meta)
180+
if err != nil {
181+
return diag.FromErr(err)
182+
}
183+
_, err = c.UpdateOutlierDetector(ctx, outlier)
184+
if err != nil {
185+
return diag.FromErr(err)
186+
}
187+
return resourceMachineLearningOutlierRead(ctx, d, meta)
188+
}
189+
190+
func resourceMachineLearningOutlierDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
191+
c := meta.(*client).mlapi
192+
err := c.DeleteOutlierDetector(ctx, d.Id())
193+
if err != nil {
194+
return diag.FromErr(err)
195+
}
196+
d.SetId("")
197+
return nil
198+
}
199+
200+
func convertToSetStructure(al mlapi.OutlierAlgorithm) []interface{} {
201+
algorithmSet := make([]interface{}, 0, 1)
202+
algorithmConfigSet := make([]interface{}, 0, 1)
203+
204+
if al.Config != nil {
205+
config := map[string]interface{}{
206+
"epsilon": al.Config.Epsilon,
207+
}
208+
algorithmConfigSet = append(algorithmConfigSet, config)
209+
}
210+
211+
algorithm := map[string]interface{}{
212+
"name": al.Name,
213+
"sensitivity": al.Sensitivity,
214+
"config": algorithmConfigSet,
215+
}
216+
algorithmSet = append(algorithmSet, algorithm)
217+
return algorithmSet
218+
}
219+
220+
func makeMLOutlier(d *schema.ResourceData, meta interface{}) (mlapi.OutlierDetector, error) {
221+
alSet := d.Get("algorithm").(*schema.Set)
222+
al := alSet.List()[0].(map[string]interface{})
223+
224+
var algorithm mlapi.OutlierAlgorithm
225+
algorithm.Name = strings.ToLower(al["name"].(string))
226+
algorithm.Sensitivity = al["sensitivity"].(float64)
227+
228+
if algorithm.Name == "dbscan" {
229+
config := new(mlapi.OutlierAlgorithmConfig)
230+
if configSet, ok := al["config"]; ok && configSet.(*schema.Set).Len() == 1 {
231+
cfg := configSet.(*schema.Set).List()[0].(map[string]interface{})
232+
config.Epsilon = cfg["epsilon"].(float64)
233+
} else {
234+
return mlapi.OutlierDetector{}, fmt.Errorf("DBSCAN algorithm requires a single \"config\" block")
235+
}
236+
algorithm.Config = config
237+
}
238+
239+
return mlapi.OutlierDetector{
240+
ID: d.Id(),
241+
Name: d.Get("name").(string),
242+
Metric: d.Get("metric").(string),
243+
Description: d.Get("description").(string),
244+
GrafanaURL: meta.(*client).gapiURL,
245+
DatasourceID: uint(d.Get("datasource_id").(int)),
246+
DatasourceUID: d.Get("datasource_uid").(string),
247+
DatasourceType: d.Get("datasource_type").(string),
248+
QueryParams: d.Get("query_params").(map[string]interface{}),
249+
Interval: uint(d.Get("interval").(int)),
250+
Algorithm: algorithm,
251+
}, nil
252+
}

0 commit comments

Comments
 (0)