diff --git a/docs/data-sources/slos.md b/docs/data-sources/slos.md
index c37a8d5a2..a96ac6296 100644
--- a/docs/data-sources/slos.md
+++ b/docs/data-sources/slos.md
@@ -214,6 +214,7 @@ Read-Only:
Read-Only:
- `freeform` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--freeform))
+- `grafana_queries` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--grafana_queries))
- `ratio` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--ratio))
- `type` (String)
@@ -225,6 +226,14 @@ Read-Only:
- `query` (String)
+
+### Nested Schema for `slos.query.grafana_queries`
+
+Read-Only:
+
+- `grafana_queries` (String)
+
+
### Nested Schema for `slos.query.ratio`
diff --git a/docs/resources/slo.md b/docs/resources/slo.md
index fc1f25b8b..60a7fc563 100644
--- a/docs/resources/slo.md
+++ b/docs/resources/slo.md
@@ -17,7 +17,58 @@ Resource manages Grafana SLOs.
## Example Usage
-### Basic
+### Ratio
+
+```terraform
+resource "grafana_slo" "ratio" {
+ name = "Terraform Testing - Ratio Query"
+ description = "Terraform Description - Ratio Query"
+ query {
+ ratio {
+ success_metric = "kubelet_http_requests_total{status!~\"5..\"}"
+ total_metric = "kubelet_http_requests_total"
+ group_by_labels = ["job", "instance"]
+ }
+ type = "ratio"
+ }
+ objectives {
+ value = 0.995
+ window = "30d"
+ }
+ destination_datasource {
+ uid = "grafanacloud-prom"
+ }
+ label {
+ key = "slo"
+ value = "terraform"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate Very High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+ }
+}
+```
+
+### Advanced
```terraform
resource "grafana_slo" "test" {
@@ -66,27 +117,54 @@ resource "grafana_slo" "test" {
}
```
-### Advanced
+### Grafana Queries - Any supported datasource
+
+Grafana Queries use the grafana_queries field. It expects a JSON string list of valid grafana query JSON objects, the same as you'll find assigned to a Grafana Dashboard panel `targets` field.
```terraform
resource "grafana_slo" "test" {
- name = "Complex Resource - Terraform Ratio Query Example"
- description = "Complex Resource - Terraform Ratio Query Description"
+ name = "Terraform Testing"
+ description = "Terraform Description"
query {
- ratio {
- success_metric = "kubelet_http_requests_total{status!~\"5..\"}"
- total_metric = "kubelet_http_requests_total"
- group_by_labels = ["job", "instance"]
+ grafana_queries {
+ grafana_queries = jsonencode([
+ {
+ datasource : {
+ "type" : "graphite",
+ "uid" : "datasource-uid"
+ },
+ refId : "Success",
+ target : "groupByNode(perSecond(web.*.http.2xx_success.*.*), 3, 'avg')"
+ },
+ {
+ datasource : {
+ "type" : "graphite",
+ "uid" : "datasource-uid"
+ },
+ refId : "Total",
+ target : "groupByNode(perSecond(web.*.http.5xx_errors.*.*), 3, 'avg')"
+ },
+ {
+ datasource : {
+ "type" : "__expr__",
+ "uid" : "__expr__"
+ },
+ expression : "$Success / $Total",
+ refId : "Expression",
+ type : "math"
+ }
+ ])
}
- type = "ratio"
+ type = "grafana_queries"
+ }
+ destination_datasource {
+ uid = "grafanacloud-prom"
}
objectives {
value = 0.995
window = "30d"
}
- destination_datasource {
- uid = "grafanacloud-prom"
- }
+
label {
key = "slo"
value = "terraform"
@@ -101,10 +179,6 @@ resource "grafana_slo" "test" {
key = "description"
value = "Error budget is burning too fast"
}
- label {
- key = "type"
- value = "slo"
- }
}
slowburn {
@@ -116,15 +190,15 @@ resource "grafana_slo" "test" {
key = "description"
value = "Error budget is burning too fast"
}
- label {
- key = "type"
- value = "slo"
- }
}
}
}
```
+For a complete list, see [supported data sources](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/additionaldatasources/#supported-data-sources).
+
+For additional help with SLOs, view our [documentation](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/).
+
## Schema
@@ -173,11 +247,12 @@ Required:
Required:
-- `type` (String) Query type must be one of: "freeform", "query", "ratio", or "threshold"
+- `type` (String) Query type must be one of: "freeform", "query", "ratio", "grafana_queries" or "threshold"
Optional:
- `freeform` (Block List, Max: 1) (see [below for nested schema](#nestedblock--query--freeform))
+- `grafana_queries` (Block List, Max: 1) Array for holding a set of grafana queries (see [below for nested schema](#nestedblock--query--grafana_queries))
- `ratio` (Block List, Max: 1) (see [below for nested schema](#nestedblock--query--ratio))
@@ -185,7 +260,15 @@ Optional:
Required:
-- `query` (String) Freeform Query Field
+- `query` (String) Freeform Query Field - valid promQl
+
+
+
+### Nested Schema for `query.grafana_queries`
+
+Required:
+
+- `grafana_queries` (String) Query Object - Array of Grafana Query JSON objects
diff --git a/examples/resources/grafana_slo/resource_appDynamics.tf b/examples/resources/grafana_slo/resource_appDynamics.tf
new file mode 100644
index 000000000..b1d77b622
--- /dev/null
+++ b/examples/resources/grafana_slo/resource_appDynamics.tf
@@ -0,0 +1,96 @@
+resource "grafana_slo" "test" {
+ name = "Terraform Testing"
+ description = "Terraform Description"
+ query {
+ grafana_queries {
+ grafana_queries = jsonencode([
+ {
+ aggregation : "Sum",
+ alias : "",
+ application : "57831",
+ applicationName : "petclinic",
+ datasource : {
+ type : "dlopes7-appdynamics-datasource",
+ uid : "appdynamics_localdev"
+ },
+ delimiter : "|",
+ isRawQuery : false,
+ metric : "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute",
+ queryType : "metrics",
+ refId : "errors",
+ rollUp : true,
+ schemaVersion : "3.9.5",
+ transformLegend : "Segments",
+ transformLegendText : ""
+ },
+ {
+ aggregation : "Sum",
+ alias : "",
+ application : "57831",
+ applicationName : "petclinic",
+ datasource : {
+ type : "dlopes7-appdynamics-datasource",
+ uid : "appdynamics_localdev"
+ },
+ intervalMs : 1000,
+ maxDataPoints : 43200,
+ delimiter : "|",
+ isRawQuery : false,
+ metric : "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute",
+ queryType : "metrics",
+ refId : "total",
+ rollUp : true,
+ schemaVersion : "3.9.5",
+ transformLegend : "Segments",
+ transformLegendText : ""
+ },
+ {
+ datasource : {
+ type : "__expr__",
+ uid : "__expr__"
+ },
+ expression : "($total - $errors) / $total",
+ intervalMs : 1000,
+ maxDataPoints : 43200,
+ refId : "C",
+ type : "math"
+ }
+ ])
+ }
+ type = "grafana_queries"
+ }
+ objectives {
+ value = 0.995
+ window = "30d"
+ }
+ destination_datasource {
+ uid = "grafanacloud-prom"
+ }
+ label {
+ key = "slo"
+ value = "terraform"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate Very High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/resources/grafana_slo/resource_graphite.tf b/examples/resources/grafana_slo/resource_graphite.tf
new file mode 100644
index 000000000..b1ec4ee77
--- /dev/null
+++ b/examples/resources/grafana_slo/resource_graphite.tf
@@ -0,0 +1,71 @@
+resource "grafana_slo" "test" {
+ name = "Terraform Testing"
+ description = "Terraform Description"
+ query {
+ grafana_queries {
+ grafana_queries = jsonencode([
+ {
+ datasource : {
+ "type" : "graphite",
+ "uid" : "datasource-uid"
+ },
+ refId : "Success",
+ target : "groupByNode(perSecond(web.*.http.2xx_success.*.*), 3, 'avg')"
+ },
+ {
+ datasource : {
+ "type" : "graphite",
+ "uid" : "datasource-uid"
+ },
+ refId : "Total",
+ target : "groupByNode(perSecond(web.*.http.5xx_errors.*.*), 3, 'avg')"
+ },
+ {
+ datasource : {
+ "type" : "__expr__",
+ "uid" : "__expr__"
+ },
+ expression : "$Success / $Total",
+ refId : "Expression",
+ type : "math"
+ }
+ ])
+ }
+ type = "grafana_queries"
+ }
+ destination_datasource {
+ uid = "grafanacloud-prom"
+ }
+ objectives {
+ value = 0.995
+ window = "30d"
+ }
+
+ label {
+ key = "slo"
+ value = "terraform"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate Very High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 8be0f7f60..67c9262bb 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
github.com/grafana/grafana-openapi-client-go v0.0.0-20241113095943-9cb2bbfeb8a3
github.com/grafana/machine-learning-go-client v0.8.2
github.com/grafana/river v0.3.0
- github.com/grafana/slo-openapi-client/go/slo v0.0.0-20240807172758-1b7d00838fc7
+ github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6
github.com/grafana/synthetic-monitoring-agent v0.34.2
github.com/grafana/synthetic-monitoring-api-go-client v0.11.0
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
diff --git a/go.sum b/go.sum
index bd02bdc44..ee76fb304 100644
--- a/go.sum
+++ b/go.sum
@@ -157,8 +157,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grafana/river v0.3.0 h1:6TsaR/vkkcppUM9I0muGbPIUedCtpPu6OWreE5+CE6g=
github.com/grafana/river v0.3.0/go.mod h1:icSidCSHYXJUYy6TjGAi/D+X7FsP7Gc7cxvBUIwYMmY=
-github.com/grafana/slo-openapi-client/go/slo v0.0.0-20240807172758-1b7d00838fc7 h1:t7zAFX0rMu868n85zRHLgmAjLJgWbkxUekGquZmovjA=
-github.com/grafana/slo-openapi-client/go/slo v0.0.0-20240807172758-1b7d00838fc7/go.mod h1:MVsmQi3lkhNnRExmke6Ug6HFG4Dycd+oRgzC3Rz+vOs=
+github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6 h1:7jbuz2MCqHlIc+vC3geOEzMZolm30v3K1bFTaTJ4mGI=
+github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6/go.mod h1:a9idYds6valDrBwBdcFTIp+QzlERgf2CFDZgEfmp9pA=
github.com/grafana/synthetic-monitoring-agent v0.34.2 h1:qGrkGapITHdRBb5oCq7tkqjIe0a6PV6icCGUiHLGvBk=
github.com/grafana/synthetic-monitoring-agent v0.34.2/go.mod h1:BVomev3uKqc5XZEWI1bxodPIAhtkcr6U5urtM8NV8e0=
github.com/grafana/synthetic-monitoring-api-go-client v0.11.0 h1:C/LMuhY9USNXRVhzQ3S09C0rSyAwCUssCp3WOShLN5I=
diff --git a/internal/resources/slo/data_source_slo.go b/internal/resources/slo/data_source_slo.go
index 7c420186b..1db2e94ee 100644
--- a/internal/resources/slo/data_source_slo.go
+++ b/internal/resources/slo/data_source_slo.go
@@ -2,6 +2,7 @@ package slo
import (
"context"
+ "encoding/json"
"github.com/grafana/slo-openapi-client/go/slo"
"github.com/grafana/terraform-provider-grafana/v3/internal/common"
@@ -123,6 +124,17 @@ func unpackQuery(apiquery slo.SloV00Query) []map[string]interface{} {
retQuery = append(retQuery, query)
}
+ if apiquery.Type == QueryTypeGrafanaQueries {
+ query := map[string]interface{}{"type": "grafana_queries"}
+
+ grafanaQueries := []map[string]interface{}{}
+ queryString, _ := json.Marshal(apiquery.GrafanaQueries.GetGrafanaQueries())
+ grafanaQueriesString := map[string]interface{}{"grafana_queries": string(queryString)}
+ grafanaQueries = append(grafanaQueries, grafanaQueriesString)
+ query["grafana_queries"] = grafanaQueries
+ retQuery = append(retQuery, query)
+ }
+
return retQuery
}
diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go
index d8714b957..d7cab3af5 100644
--- a/internal/resources/slo/resource_slo.go
+++ b/internal/resources/slo/resource_slo.go
@@ -2,22 +2,25 @@ package slo
import (
"context"
+ "encoding/json"
"fmt"
"regexp"
"github.com/grafana/slo-openapi-client/go/slo"
"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+ "github.com/hashicorp/go-cty/cty"
"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"
)
const (
- QueryTypeFreeform string = "freeform"
- QueryTypeHistogram string = "histogram"
- QueryTypeRatio string = "ratio"
- QueryTypeThreshold string = "threshold"
+ QueryTypeFreeform string = "freeform"
+ QueryTypeHistogram string = "histogram"
+ QueryTypeRatio string = "ratio"
+ QueryTypeThreshold string = "threshold"
+ QueryTypeGrafanaQueries string = "grafanaQueries"
)
var resourceSloID = common.NewResourceID(common.StringIDField("uuid"))
@@ -79,8 +82,8 @@ Resource manages Grafana SLOs.
Schema: map[string]*schema.Schema{
"type": {
Type: schema.TypeString,
- Description: `Query type must be one of: "freeform", "query", "ratio", or "threshold"`,
- ValidateFunc: validation.StringInSlice([]string{"freeform", "query", "ratio", "threshold"}, false),
+ Description: `Query type must be one of: "freeform", "query", "ratio", "grafana_queries" or "threshold"`,
+ ValidateFunc: validation.StringInSlice([]string{"freeform", "query", "ratio", "threshold", "grafana_queries"}, false),
Required: true,
},
"freeform": {
@@ -92,7 +95,23 @@ Resource manages Grafana SLOs.
"query": {
Type: schema.TypeString,
Required: true,
- Description: "Freeform Query Field",
+ Description: "Freeform Query Field - valid promQl",
+ },
+ },
+ },
+ },
+ "grafana_queries": {
+ Type: schema.TypeList,
+ MaxItems: 1,
+ Optional: true,
+ Description: "Array for holding a set of grafana queries",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "grafana_queries": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "Query Object - Array of Grafana Query JSON objects",
+ ValidateDiagFunc: ValidateGrafanaQuery(),
},
},
},
@@ -511,6 +530,33 @@ func packQuery(query map[string]interface{}) (slo.SloV00Query, error) {
return sloQuery, nil
}
+ if query["type"] == "grafana_queries" {
+ // This is safe
+ grafanaInterface := query["grafana_queries"].([]interface{})
+
+ if len(grafanaInterface) == 0 {
+ return slo.SloV00Query{}, fmt.Errorf("grafana_queries must be set")
+ }
+
+ grafanaquery := grafanaInterface[0].(map[string]interface{})
+ querystring := grafanaquery["grafana_queries"].(string)
+
+ var queryMapList []map[string]interface{}
+ err := json.Unmarshal([]byte(querystring), &queryMapList)
+
+ // We validate the JSON structure this should never occur
+ if err != nil {
+ return slo.SloV00Query{}, err
+ }
+
+ sloQuery := slo.SloV00Query{
+ GrafanaQueries: &slo.SloV00GrafanaQueries{GrafanaQueries: queryMapList},
+ Type: QueryTypeGrafanaQueries,
+ }
+
+ return sloQuery, nil
+ }
+
return slo.SloV00Query{}, fmt.Errorf("%s query type not implemented", query["type"])
}
@@ -661,3 +707,92 @@ func apiError(action string, err error) diag.Diagnostics {
},
}
}
+
+func ValidateGrafanaQuery() schema.SchemaValidateDiagFunc {
+ return func(i interface{}, path cty.Path) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ v, ok := i.(string)
+ if !ok {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Bad Format",
+ Detail: fmt.Sprintf("expected type of %s to be string", path),
+ AttributePath: path,
+ })
+ return diags
+ }
+
+ var gmrQuery []map[string]any
+ err := json.Unmarshal([]byte(v), &gmrQuery)
+ if err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Bad Format",
+ Detail: "expected grafana queries to be valid JSON format",
+ AttributePath: path,
+ })
+ return diags
+ }
+
+ if len(gmrQuery) == 0 {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: "expected grafana queries to have at least one query",
+ AttributePath: path,
+ })
+ return diags
+ }
+
+ for _, queryObj := range gmrQuery {
+ currentPath := path.Copy()
+
+ refID, ok := queryObj["refId"]
+ if !ok {
+ // This unmarshalled so it is safe to marshal
+ obj, _ := json.Marshal(queryObj)
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: fmt.Sprintf("expected grafana query to have a 'refId' field (%s)", obj),
+ AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("refId")}),
+ })
+ return diags
+ }
+
+ source := queryObj["datasource"]
+ s, ok := source.(map[string]interface{})
+ if !ok {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: fmt.Sprintf("expected grafana query to have a 'datasource' field (refId:%s)", refID),
+ AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("datasource")}),
+ })
+ return diags
+ }
+
+ currentPath = append(currentPath, cty.IndexStep{Key: cty.StringVal("datasource")})
+ _, ok = s["type"]
+ if !ok {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: fmt.Sprintf("expected grafana query datasource field to have a 'type' field (refId:%s)", refID),
+ AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("type")}),
+ })
+ }
+ _, ok = s["uid"]
+ if !ok {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: fmt.Sprintf("expected grafana query datasource field to have a 'uid' field (refId:%s)", refID),
+ AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("uid")}),
+ })
+ }
+ }
+ return diags
+ }
+}
diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go
index 8623c0fec..e08ba6276 100644
--- a/internal/resources/slo/resource_slo_test.go
+++ b/internal/resources/slo/resource_slo_test.go
@@ -2,6 +2,7 @@ package slo_test
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"regexp"
@@ -9,6 +10,12 @@ import (
"testing"
"time"
+ "github.com/grafana/grafana-openapi-client-go/models"
+ slo2 "github.com/grafana/terraform-provider-grafana/v3/internal/resources/slo"
+ "github.com/hashicorp/go-cty/cty"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/stretchr/testify/assert"
+
"github.com/grafana/slo-openapi-client/go/slo"
"github.com/grafana/terraform-provider-grafana/v3/internal/common"
"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
@@ -350,6 +357,80 @@ resource "grafana_slo" "invalid" {
}
`
+const graphiteBadFormat = `
+resource "grafana_slo" "invalid" {
+ name = "Terraform Testing"
+ description = "Terraform Description"
+ query {
+ grafana_queries {
+ grafana_queries = jsonencode([
+ {
+ "datasource": {
+ "type": "graphite",
+ "uid": "grafanacloud-graphite"
+ },
+ "refId": "Success",
+ "target": "groupByNode(perSecond(web.*.http.2xx_success.*.*), 1, 'avg''')"
+ },
+ {
+ "datasource": {
+ "type": "graphite",
+ "uid": "grafanacloud-graphite"
+ },
+ "refId": "Total",
+ "target": "groupByNode(perSecond(web.*.http.*.*.*), 1, 'avg')"
+ },
+ {
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__"
+ },
+ "expression": "$Success / $Total",
+ "refId": "Expression",
+ "type": "math"
+ }
+])
+ }
+ type = "grafana_queries"
+ }
+ destination_datasource {
+ uid = "grafanacloud-prom"
+ }
+ objectives {
+ value = 0.995
+ window = "30d"
+ }
+
+ label {
+ key = "slo"
+ value = "terraform"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate Very High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "SLO Burn Rate High"
+ }
+ annotation {
+ key = "description"
+ value = "Error budget is burning too fast"
+ }
+ }
+ }
+}
+`
+
func emptyAlert(name string) string {
return fmt.Sprintf(`
resource "grafana_slo" "empty_alert" {
@@ -409,6 +490,169 @@ func TestAccResourceInvalidSlo(t *testing.T) {
Config: sloMissingDestinationDatasource,
ExpectError: regexp.MustCompile("Error: Insufficient destination_datasource blocks"),
},
+ {
+ Config: graphiteBadFormat,
+ ExpectError: regexp.MustCompile("Error: Unable to create SLO - API"),
+ },
},
})
}
+
+func TestValidateGrafanaQuery(t *testing.T) {
+ tests := map[string]struct {
+ query string
+ expectedDiags diag.Diagnostics
+ }{
+ "prometheus": {
+ query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))",
+ expectedDiags: diag.Diagnostics{diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Bad Format",
+ Detail: "expected grafana queries to be valid JSON format",
+ AttributePath: cty.IndexPath(cty.Value{}),
+ }},
+ },
+ "grafanaQueries_success": {
+ query: createGrafanaQuery(true, []map[string]any{}),
+ expectedDiags: diag.Diagnostics{},
+ },
+ "grafanaQueries_noRefId": {
+ query: createGrafanaQuery(false, []map[string]any{{}}),
+ expectedDiags: diag.Diagnostics{
+ diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: fmt.Sprintf("expected grafana query to have a 'refId' field (%s)", "{}"),
+ AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("refId")}),
+ },
+ },
+ },
+ "grafanaQueries_noDatasource": {
+ query: createGrafanaQuery(false, []map[string]any{{"refId": "A"}}),
+ expectedDiags: diag.Diagnostics{
+ diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: "expected grafana query to have a 'datasource' field (refId:A)",
+ AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("datasource")}),
+ },
+ },
+ },
+ "grafanaQueries_missingFields": {
+ query: createGrafanaQuery(false, []map[string]any{{"refId": "A", "datasource": models.DataSource{}}}),
+ expectedDiags: diag.Diagnostics{
+ diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: "expected grafana query datasource field to have a 'type' field (refId:A)",
+ AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("datasource")}, cty.IndexStep{Key: cty.StringVal("type")}),
+ },
+ diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Missing Required Field",
+ Detail: "expected grafana query datasource field to have a 'uid' field (refId:A)",
+ AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("datasource")}, cty.IndexStep{Key: cty.StringVal("uid")}),
+ },
+ },
+ },
+ }
+ testFunc := slo2.ValidateGrafanaQuery()
+
+ for name, tc := range tests {
+ t.Run(name, func(t *testing.T) {
+ diags := testFunc(tc.query, cty.IndexPath(cty.Value{}))
+
+ require.Len(t, diags, len(tc.expectedDiags))
+ for i, w := range tc.expectedDiags {
+ assert.Equal(t, w, diags[i])
+ }
+ })
+ }
+}
+
+func createGrafanaQuery(useDefault bool, input []map[string]any) string {
+ const grafanaQueriesQuery = `
+ [
+ {
+ "aggregation": "Sum",
+ "alias": "",
+ "application": "57831",
+ "applicationName": "petclinic",
+ "datasource": {
+ "type": "dlopes7-appdynamics-datasource",
+ "uid": "appdynamics_localdev"
+ },
+ "delimiter": "|",
+ "isRawQuery": false,
+ "metric": "Overall Application Performance|Calls per Minute",
+ "queryType": "metrics",
+ "refId": "total",
+ "rollUp": true,
+ "schemaVersion": "3.9.5",
+ "transformLegend": "Segments",
+ "transformLegendText": ""
+ },
+ {
+ "aggregation": "Sum",
+ "alias": "",
+ "application": "57831",
+ "applicationName": "petclinic",
+ "datasource": {
+ "type": "dlopes7-appdynamics-datasource",
+ "uid": "appdynamics_localdev"
+ },
+ "intervalMs": 1000,
+ "maxDataPoints": 43200,
+ "delimiter": "|",
+ "isRawQuery": false,
+ "metric": "Overall Application Performance|Calls per Minute",
+ "queryType": "metrics",
+ "refId": "also_total",
+ "rollUp": true,
+ "schemaVersion": "3.9.5",
+ "transformLegend": "Segments",
+ "transformLegendText": ""
+ },
+ {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ 0
+ ],
+ "type": "gt"
+ },
+ "operator": {
+ "type": "and"
+ },
+ "query": {
+ "params": []
+ },
+ "reducer": {
+ "params": [],
+ "type": "avg"
+ },
+ "type": "query"
+ }
+ ],
+ "datasource": {
+ "name": "Expression",
+ "type": "__expr__",
+ "uid": "__expr__"
+ },
+ "expression": "($total / $also_total)",
+ "intervalMs": 1000,
+ "maxDataPoints": 43200,
+ "refId": "C",
+ "type": "math"
+ }
+ ]`
+
+ if useDefault {
+ return grafanaQueriesQuery
+ }
+
+ output, _ := json.Marshal(input)
+ return string(output)
+}
diff --git a/templates/resources/slo.md.tmpl b/templates/resources/slo.md.tmpl
index 330801ef3..0a675ada5 100644
--- a/templates/resources/slo.md.tmpl
+++ b/templates/resources/slo.md.tmpl
@@ -12,13 +12,23 @@ description: |-
## Example Usage
-### Basic
+### Ratio
-{{ tffile "examples/resources/grafana_slo/resource.tf" }}
+{{ tffile "examples/resources/grafana_slo/resource_ratio.tf" }}
### Advanced
-{{ tffile "examples/resources/grafana_slo/resource_complex.tf" }}
+{{ tffile "examples/resources/grafana_slo/resource.tf" }}
+
+### Grafana Queries - Any supported datasource
+
+Grafana Queries use the grafana_queries field. It expects a JSON string list of valid grafana query JSON objects, the same as you'll find assigned to a Grafana Dashboard panel `targets` field.
+
+{{ tffile "examples/resources/grafana_slo/resource_graphite.tf" }}
+
+For a complete list, see [supported data sources](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/set-up/additionaldatasources/#supported-data-sources).
+
+For additional help with SLOs, view our [documentation](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/).
{{ .SchemaMarkdown | trimspace }}