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 }}