diff --git a/CHANGELOG.md b/CHANGELOG.md index 284554342..c2e78b535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +- Add support for `timeslice_metric_indicator` in `elasticstack_kibana_slo` ([#1195](https://github.com/elastic/terraform-provider-elasticstack/pull/1195)) + ## [0.11.16] - 2025-07-09 - Add `headers` for the provider connection ([#1057](https://github.com/elastic/terraform-provider-elasticstack/pull/1057)) diff --git a/docs/resources/kibana_slo.md b/docs/resources/kibana_slo.md index 9845b31ee..c5759e012 100644 --- a/docs/resources/kibana_slo.md +++ b/docs/resources/kibana_slo.md @@ -190,6 +190,41 @@ resource "elasticstack_kibana_slo" "custom_metric" { timeslice_window = "5m" } +} + +//Available from 8.12.0 +resource "elasticstack_kibana_slo" "timeslice_metric" { + name = "timeslice metric" + description = "timeslice metric" + + timeslice_metric_indicator { + index = "my-index" + timestamp_field = "@timestamp" + metric { + metrics { + name = "A" + aggregation = "sum" + field = "latency" + } + equation = "A" + comparator = "GT" + threshold = 100 + } + } + + time_window { + duration = "7d" + type = "rolling" + } + + budgeting_method = "timeslices" + + objective { + target = 0.95 + timeslice_target = 0.95 + timeslice_window = "5m" + } + } ``` @@ -216,6 +251,7 @@ resource "elasticstack_kibana_slo" "custom_metric" { - `slo_id` (String) An ID (8 and 36 characters). If omitted, a UUIDv1 will be generated server-side. - `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. - `tags` (List of String) The tags for the SLO. +- `timeslice_metric_indicator` (Block List, Max: 1) Defines a timeslice metric indicator for SLO. (see [below for nested schema](#nestedblock--timeslice_metric_indicator)) ### Read-Only @@ -405,6 +441,44 @@ Optional: - `frequency` (String) - `sync_delay` (String) + + +### Nested Schema for `timeslice_metric_indicator` + +Required: + +- `index` (String) +- `metric` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--timeslice_metric_indicator--metric)) +- `timestamp_field` (String) + +Optional: + +- `filter` (String) + + +### Nested Schema for `timeslice_metric_indicator.metric` + +Required: + +- `comparator` (String) +- `equation` (String) +- `metrics` (Block List, Min: 1) (see [below for nested schema](#nestedblock--timeslice_metric_indicator--metric--metrics)) +- `threshold` (Number) + + +### Nested Schema for `timeslice_metric_indicator.metric.metrics` + +Required: + +- `aggregation` (String) The aggregation type for this metric. One of: sum, avg, min, max, value_count, percentile, doc_count. Determines which other fields are required: +- `name` (String) The unique name for this metric. Used as a variable in the equation field. + +Optional: + +- `field` (String) Field to aggregate. Required for aggregations: sum, avg, min, max, value_count, percentile. Must NOT be set for doc_count. +- `filter` (String) Optional KQL filter for this metric. Supported for all aggregations except doc_count. +- `percentile` (Number) Percentile value (e.g., 99). Required if aggregation is 'percentile'. Must NOT be set for other aggregations. + ## Import Import is supported using the following syntax: diff --git a/examples/resources/elasticstack_kibana_slo/resource.tf b/examples/resources/elasticstack_kibana_slo/resource.tf index be8b94aa3..1fa853ba0 100644 --- a/examples/resources/elasticstack_kibana_slo/resource.tf +++ b/examples/resources/elasticstack_kibana_slo/resource.tf @@ -176,3 +176,38 @@ resource "elasticstack_kibana_slo" "custom_metric" { } } + +//Available from 8.12.0 +resource "elasticstack_kibana_slo" "timeslice_metric" { + name = "timeslice metric" + description = "timeslice metric" + + timeslice_metric_indicator { + index = "my-index" + timestamp_field = "@timestamp" + metric { + metrics { + name = "A" + aggregation = "sum" + field = "latency" + } + equation = "A" + comparator = "GT" + threshold = 100 + } + } + + time_window { + duration = "7d" + type = "rolling" + } + + budgeting_method = "timeslices" + + objective { + target = 0.95 + timeslice_target = 0.95 + timeslice_window = "5m" + } + +} \ No newline at end of file diff --git a/generated/slo-spec.yml b/generated/slo-spec.yml index a1922fa03..d42e6831c 100644 --- a/generated/slo-spec.yml +++ b/generated/slo-spec.yml @@ -969,6 +969,18 @@ components: description: List of metrics with their name, aggregation type, and field. type: array items: + discriminator: + propertyName: aggregation + mapping: + percentile: '#/components/schemas/timeslice_metric_percentile_metric' + doc_count: '#/components/schemas/timeslice_metric_doc_count_metric' + sum: '#/components/schemas/timeslice_metric_basic_metric_with_field' + avg: '#/components/schemas/timeslice_metric_basic_metric_with_field' + min: '#/components/schemas/timeslice_metric_basic_metric_with_field' + max: '#/components/schemas/timeslice_metric_basic_metric_with_field' + std_deviation: '#/components/schemas/timeslice_metric_basic_metric_with_field' + last_value: '#/components/schemas/timeslice_metric_basic_metric_with_field' + cardinality: '#/components/schemas/timeslice_metric_basic_metric_with_field' anyOf: - $ref: '#/components/schemas/timeslice_metric_basic_metric_with_field' - $ref: '#/components/schemas/timeslice_metric_percentile_metric' diff --git a/generated/slo/api/openapi.yaml b/generated/slo/api/openapi.yaml index 6533b9907..c854dbc3c 100644 --- a/generated/slo/api/openapi.yaml +++ b/generated/slo/api/openapi.yaml @@ -1705,6 +1705,18 @@ components: - $ref: '#/components/schemas/timeslice_metric_basic_metric_with_field' - $ref: '#/components/schemas/timeslice_metric_percentile_metric' - $ref: '#/components/schemas/timeslice_metric_doc_count_metric' + discriminator: + mapping: + percentile: '#/components/schemas/timeslice_metric_percentile_metric' + doc_count: '#/components/schemas/timeslice_metric_doc_count_metric' + sum: '#/components/schemas/timeslice_metric_basic_metric_with_field' + avg: '#/components/schemas/timeslice_metric_basic_metric_with_field' + min: '#/components/schemas/timeslice_metric_basic_metric_with_field' + max: '#/components/schemas/timeslice_metric_basic_metric_with_field' + std_deviation: '#/components/schemas/timeslice_metric_basic_metric_with_field' + last_value: '#/components/schemas/timeslice_metric_basic_metric_with_field' + cardinality: '#/components/schemas/timeslice_metric_basic_metric_with_field' + propertyName: aggregation indicator_properties_timeslice_metric_params_metric: description: | An object defining the metrics, equation, and threshold to determine if it's a good slice or not diff --git a/generated/slo/model_indicator_properties_timeslice_metric_params_metric_metrics_inner.go b/generated/slo/model_indicator_properties_timeslice_metric_params_metric_metrics_inner.go index 828f796ee..afbce10b1 100644 --- a/generated/slo/model_indicator_properties_timeslice_metric_params_metric_metrics_inner.go +++ b/generated/slo/model_indicator_properties_timeslice_metric_params_metric_metrics_inner.go @@ -25,6 +25,205 @@ type IndicatorPropertiesTimesliceMetricParamsMetricMetricsInner struct { // Unmarshal JSON data into any of the pointers in the struct func (dst *IndicatorPropertiesTimesliceMetricParamsMetricMetricsInner) UnmarshalJSON(data []byte) error { var err error + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = json.Unmarshal(data, &jsonDict) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON into map for the discriminator lookup") + } + + // check if the discriminator value is 'avg' + if jsonDict["aggregation"] == "avg" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'cardinality' + if jsonDict["aggregation"] == "cardinality" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'doc_count' + if jsonDict["aggregation"] == "doc_count" { + // try to unmarshal JSON data into TimesliceMetricDocCountMetric + err = json.Unmarshal(data, &dst.TimesliceMetricDocCountMetric) + if err == nil { + jsonTimesliceMetricDocCountMetric, _ := json.Marshal(dst.TimesliceMetricDocCountMetric) + if string(jsonTimesliceMetricDocCountMetric) == "{}" { // empty struct + dst.TimesliceMetricDocCountMetric = nil + } else { + return nil // data stored in dst.TimesliceMetricDocCountMetric, return on the first match + } + } else { + dst.TimesliceMetricDocCountMetric = nil + } + } + + // check if the discriminator value is 'last_value' + if jsonDict["aggregation"] == "last_value" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'max' + if jsonDict["aggregation"] == "max" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'min' + if jsonDict["aggregation"] == "min" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'percentile' + if jsonDict["aggregation"] == "percentile" { + // try to unmarshal JSON data into TimesliceMetricPercentileMetric + err = json.Unmarshal(data, &dst.TimesliceMetricPercentileMetric) + if err == nil { + jsonTimesliceMetricPercentileMetric, _ := json.Marshal(dst.TimesliceMetricPercentileMetric) + if string(jsonTimesliceMetricPercentileMetric) == "{}" { // empty struct + dst.TimesliceMetricPercentileMetric = nil + } else { + return nil // data stored in dst.TimesliceMetricPercentileMetric, return on the first match + } + } else { + dst.TimesliceMetricPercentileMetric = nil + } + } + + // check if the discriminator value is 'std_deviation' + if jsonDict["aggregation"] == "std_deviation" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'sum' + if jsonDict["aggregation"] == "sum" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'timeslice_metric_basic_metric_with_field' + if jsonDict["aggregation"] == "timeslice_metric_basic_metric_with_field" { + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField + err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) + if err == nil { + jsonTimesliceMetricBasicMetricWithField, _ := json.Marshal(dst.TimesliceMetricBasicMetricWithField) + if string(jsonTimesliceMetricBasicMetricWithField) == "{}" { // empty struct + dst.TimesliceMetricBasicMetricWithField = nil + } else { + return nil // data stored in dst.TimesliceMetricBasicMetricWithField, return on the first match + } + } else { + dst.TimesliceMetricBasicMetricWithField = nil + } + } + + // check if the discriminator value is 'timeslice_metric_doc_count_metric' + if jsonDict["aggregation"] == "timeslice_metric_doc_count_metric" { + // try to unmarshal JSON data into TimesliceMetricDocCountMetric + err = json.Unmarshal(data, &dst.TimesliceMetricDocCountMetric) + if err == nil { + jsonTimesliceMetricDocCountMetric, _ := json.Marshal(dst.TimesliceMetricDocCountMetric) + if string(jsonTimesliceMetricDocCountMetric) == "{}" { // empty struct + dst.TimesliceMetricDocCountMetric = nil + } else { + return nil // data stored in dst.TimesliceMetricDocCountMetric, return on the first match + } + } else { + dst.TimesliceMetricDocCountMetric = nil + } + } + + // check if the discriminator value is 'timeslice_metric_percentile_metric' + if jsonDict["aggregation"] == "timeslice_metric_percentile_metric" { + // try to unmarshal JSON data into TimesliceMetricPercentileMetric + err = json.Unmarshal(data, &dst.TimesliceMetricPercentileMetric) + if err == nil { + jsonTimesliceMetricPercentileMetric, _ := json.Marshal(dst.TimesliceMetricPercentileMetric) + if string(jsonTimesliceMetricPercentileMetric) == "{}" { // empty struct + dst.TimesliceMetricPercentileMetric = nil + } else { + return nil // data stored in dst.TimesliceMetricPercentileMetric, return on the first match + } + } else { + dst.TimesliceMetricPercentileMetric = nil + } + } + // try to unmarshal JSON data into TimesliceMetricBasicMetricWithField err = json.Unmarshal(data, &dst.TimesliceMetricBasicMetricWithField) if err == nil { diff --git a/internal/clients/kibana/slo.go b/internal/clients/kibana/slo.go index e0bf9f2a3..58863044f 100644 --- a/internal/clients/kibana/slo.go +++ b/internal/clients/kibana/slo.go @@ -156,6 +156,9 @@ func responseIndicatorToCreateSloRequestIndicator(s slo.SloResponseIndicator) (s case *slo.IndicatorPropertiesHistogram: ret.IndicatorPropertiesHistogram = ind + case *slo.IndicatorPropertiesTimesliceMetric: + ret.IndicatorPropertiesTimesliceMetric = ind + default: return ret, fmt.Errorf("unknown indicator type: %T", ind) } diff --git a/internal/kibana/slo.go b/internal/kibana/slo.go index ce8c6bd8c..c09b82838 100644 --- a/internal/kibana/slo.go +++ b/internal/kibana/slo.go @@ -390,6 +390,86 @@ func getSchema() map[string]*schema.Schema { }, }, }, + "timeslice_metric_indicator": { + Description: "Defines a timeslice metric indicator for SLO.", + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Optional: true, + ExactlyOneOf: indicatorAddresses, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "index": { + Type: schema.TypeString, + Required: true, + }, + "timestamp_field": { + Type: schema.TypeString, + Required: true, + }, + "filter": { + Type: schema.TypeString, + Optional: true, + }, + "metric": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metrics": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The unique name for this metric. Used as a variable in the equation field.", + }, + "aggregation": { + Type: schema.TypeString, + Required: true, + Description: "The aggregation type for this metric. One of: sum, avg, min, max, value_count, percentile, doc_count. Determines which other fields are required:", + }, + "field": { + Type: schema.TypeString, + Optional: true, + Description: "Field to aggregate. Required for aggregations: sum, avg, min, max, value_count, percentile. Must NOT be set for doc_count.", + }, + "percentile": { + Type: schema.TypeFloat, + Optional: true, + Description: "Percentile value (e.g., 99). Required if aggregation is 'percentile'. Must NOT be set for other aggregations.", + }, + "filter": { + Type: schema.TypeString, + Optional: true, + Description: "Optional KQL filter for this metric. Supported for all aggregations except doc_count.", + }, + }, + }, + }, + "equation": { + Type: schema.TypeString, + Required: true, + }, + "comparator": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"GT", "GTE", "LT", "LTE"}, false), + }, + "threshold": { + Type: schema.TypeFloat, + Required: true, + }, + }, + }, + }, + }, + }, + }, "time_window": { Description: "Currently support `calendarAligned` and `rolling` time windows. Any duration greater than 1 day can be used: days, weeks, months, quarters, years. Rolling time window requires a duration, e.g. `1w` for one week, and type: `rolling`. SLOs defined with such time window, will only consider the SLI data from the last duration period as a moving window. Calendar aligned time window requires a duration, limited to `1M` for monthly or `1w` for weekly, and type: `calendarAligned`.", Type: schema.TypeList, @@ -634,6 +714,60 @@ func getSloFromResourceData(d *schema.ResourceData) (models.Slo, diag.Diagnostic }, } + case "timeslice_metric_indicator": + params := d.Get("timeslice_metric_indicator.0").(map[string]interface{}) + metricBlock := params["metric"].([]interface{})[0].(map[string]interface{}) + metricsIface := metricBlock["metrics"].([]interface{}) + metrics := make([]slo.IndicatorPropertiesTimesliceMetricParamsMetricMetricsInner, len(metricsIface)) + for i, m := range metricsIface { + metric := m.(map[string]interface{}) + agg := metric["aggregation"].(string) + switch agg { + case "sum", "avg", "min", "max", "value_count": + metrics[i] = slo.IndicatorPropertiesTimesliceMetricParamsMetricMetricsInner{ + TimesliceMetricBasicMetricWithField: &slo.TimesliceMetricBasicMetricWithField{ + Name: metric["name"].(string), + Aggregation: agg, + Field: metric["field"].(string), + }, + } + case "percentile": + metrics[i] = slo.IndicatorPropertiesTimesliceMetricParamsMetricMetricsInner{ + TimesliceMetricPercentileMetric: &slo.TimesliceMetricPercentileMetric{ + Name: metric["name"].(string), + Aggregation: agg, + Field: metric["field"].(string), + Percentile: metric["percentile"].(float64), + }, + } + case "doc_count": + metrics[i] = slo.IndicatorPropertiesTimesliceMetricParamsMetricMetricsInner{ + TimesliceMetricDocCountMetric: &slo.TimesliceMetricDocCountMetric{ + Name: metric["name"].(string), + Aggregation: agg, + }, + } + default: + return models.Slo{}, diag.Errorf("metrics[%d]: unsupported aggregation '%s'", i, agg) + } + } + indicator = slo.SloResponseIndicator{ + IndicatorPropertiesTimesliceMetric: &slo.IndicatorPropertiesTimesliceMetric{ + Type: indicatorAddressToType[indicatorType], + Params: slo.IndicatorPropertiesTimesliceMetricParams{ + Index: params["index"].(string), + TimestampField: params["timestamp_field"].(string), + Filter: getOrNilString("timeslice_metric_indicator.0.filter", d), + Metric: slo.IndicatorPropertiesTimesliceMetricParamsMetric{ + Metrics: metrics, + Equation: metricBlock["equation"].(string), + Comparator: metricBlock["comparator"].(string), + Threshold: metricBlock["threshold"].(float64), + }, + }, + }, + } + default: return models.Slo{}, diag.Errorf("unknown indicator type %s", indicatorType) } @@ -873,6 +1007,42 @@ func resourceSloRead(ctx context.Context, d *schema.ResourceData, meta interface "total": total, }) + case s.Indicator.IndicatorPropertiesTimesliceMetric != nil: + indicatorAddress = indicatorTypeToAddress[s.Indicator.IndicatorPropertiesTimesliceMetric.Type] + params := s.Indicator.IndicatorPropertiesTimesliceMetric.Params + metrics := []map[string]interface{}{} + for _, m := range params.Metric.Metrics { + metric := map[string]interface{}{} + if m.TimesliceMetricBasicMetricWithField != nil { + metric["name"] = m.TimesliceMetricBasicMetricWithField.Name + metric["aggregation"] = m.TimesliceMetricBasicMetricWithField.Aggregation + metric["field"] = m.TimesliceMetricBasicMetricWithField.Field + } + if m.TimesliceMetricPercentileMetric != nil { + metric["name"] = m.TimesliceMetricPercentileMetric.Name + metric["aggregation"] = m.TimesliceMetricPercentileMetric.Aggregation + metric["field"] = m.TimesliceMetricPercentileMetric.Field + metric["percentile"] = m.TimesliceMetricPercentileMetric.Percentile + } + if m.TimesliceMetricDocCountMetric != nil { + metric["name"] = m.TimesliceMetricDocCountMetric.Name + metric["aggregation"] = m.TimesliceMetricDocCountMetric.Aggregation + } + metrics = append(metrics, metric) + } + metricBlock := map[string]interface{}{ + "metrics": metrics, + "equation": params.Metric.Equation, + "comparator": params.Metric.Comparator, + "threshold": params.Metric.Threshold, + } + indicator = append(indicator, map[string]interface{}{ + "index": params.Index, + "timestamp_field": params.TimestampField, + "filter": params.Filter, + "metric": []interface{}{metricBlock}, + }) + default: return diag.Errorf("indicator not set") } @@ -964,6 +1134,7 @@ var indicatorAddressToType = map[string]string{ "kql_custom_indicator": "sli.kql.custom", "metric_custom_indicator": "sli.metric.custom", "histogram_custom_indicator": "sli.histogram.custom", + "timeslice_metric_indicator": "sli.metric.timeslice", } var indicatorTypeToAddress = utils.FlipMap(indicatorAddressToType) diff --git a/internal/kibana/slo_test.go b/internal/kibana/slo_test.go index e1523bc7c..ed41a5c7b 100644 --- a/internal/kibana/slo_test.go +++ b/internal/kibana/slo_test.go @@ -21,6 +21,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) +var sloTimesliceMetricsMinVersion = version.Must(version.NewVersion("8.12.0")) + func TestAccResourceSlo(t *testing.T) { // This test exposes a bug in Kibana present in 8.11.x slo8_9Constraints, err := version.NewConstraint(">=8.9.0,!=8.11.0,!=8.11.1,!=8.11.2,!=8.11.3,!=8.11.4") @@ -155,6 +157,21 @@ func TestAccResourceSlo(t *testing.T) { resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "tags.1", "another_tag"), ), }, + { + SkipFunc: versionutils.CheckIfVersionIsUnsupported(sloTimesliceMetricsMinVersion), + Config: getSLOConfig(sloVars{ + name: sloName, + indicatorType: "timeslice_metric_indicator", + settingsEnabled: true, + tags: []string{"tag-1", "another_tag"}, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.index", "my-index-"+sloName), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.equation", "A"), + ), + }, }, }) } @@ -237,6 +254,281 @@ func TestAccResourceSloGroupBy(t *testing.T) { }) } +func TestAccResourceSlo_timeslice_metric_indicator_basic(t *testing.T) { + sloName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSloDestroy, + Steps: []resource.TestStep{ + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(sloTimesliceMetricsMinVersion), + Config: fmt.Sprintf(` + provider "elasticstack" { + elasticsearch {} + kibana {} + } + + resource "elasticstack_elasticsearch_index" "my_index" { + name = "my-index" + deletion_protection = false + } + resource "elasticstack_kibana_slo" "test_slo" { + name = "%s" + description = "basic timeslice metric" + timeslice_metric_indicator { + index = "my-index" + timestamp_field = "@timestamp" + filter = "status_code: 200" + metric { + metrics { + name = "A" + aggregation = "sum" + field = "latency" + } + equation = "A" + comparator = "GT" + threshold = 100 + } + } + budgeting_method = "timeslices" + objective { + target = 0.95 + timeslice_target = 0.95 + timeslice_window = "5m" + } + time_window { + duration = "7d" + type = "rolling" + } + depends_on = [elasticstack_elasticsearch_index.my_index] + } + `, sloName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.timestamp_field", "@timestamp"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.filter", "status_code: 200"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.aggregation", "sum"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.field", "latency"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.equation", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.comparator", "GT"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.threshold", "100"), + ), + }, + }, + }) +} + +func TestAccResourceSlo_timeslice_metric_indicator_percentile(t *testing.T) { + sloName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSloDestroy, + Steps: []resource.TestStep{ + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(sloTimesliceMetricsMinVersion), + Config: fmt.Sprintf(` + provider "elasticstack" { + elasticsearch {} + kibana {} + } + + resource "elasticstack_elasticsearch_index" "my_index" { + name = "my-index" + deletion_protection = false + } + + resource "elasticstack_kibana_slo" "test_slo" { + name = "%s" + description = "percentile timeslice metric" + timeslice_metric_indicator { + index = "my-index" + timestamp_field = "@timestamp" + metric { + metrics { + name = "B" + aggregation = "percentile" + field = "latency" + percentile = 99 + } + equation = "B" + comparator = "LT" + threshold = 200 + } + } + budgeting_method = "timeslices" + objective { + target = 0.95 + timeslice_target = 0.95 + timeslice_window = "5m" + } + time_window { + duration = "7d" + type = "rolling" + } + depends_on = [elasticstack_elasticsearch_index.my_index] + } + `, sloName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.timestamp_field", "@timestamp"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.filter", ""), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.name", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.aggregation", "percentile"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.percentile", "99"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.equation", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.comparator", "LT"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.threshold", "200"), + ), + }, + }, + }) +} + +func TestAccResourceSlo_timeslice_metric_indicator_doc_count(t *testing.T) { + sloName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSloDestroy, + Steps: []resource.TestStep{ + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(sloTimesliceMetricsMinVersion), + Config: fmt.Sprintf(` + provider "elasticstack" { + elasticsearch {} + kibana {} + } + + resource "elasticstack_elasticsearch_index" "my_index" { + name = "my-index" + deletion_protection = false + } + + resource "elasticstack_kibana_slo" "test_slo" { + name = "%s" + description = "doc_count timeslice metric" + timeslice_metric_indicator { + index = "my-index" + timestamp_field = "@timestamp" + metric { + metrics { + name = "C" + aggregation = "doc_count" + } + equation = "C" + comparator = "GTE" + threshold = 10 + } + } + budgeting_method = "timeslices" + objective { + target = 0.95 + timeslice_target = 0.95 + timeslice_window = "5m" + } + time_window { + duration = "7d" + type = "rolling" + } + depends_on = [elasticstack_elasticsearch_index.my_index] + } + `, sloName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.timestamp_field", "@timestamp"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.name", "C"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.aggregation", "doc_count"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.equation", "C"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.comparator", "GTE"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.threshold", "10"), + ), + }, + }, + }) +} + +func TestAccResourceSlo_timeslice_metric_indicator_multiple_mixed_metrics(t *testing.T) { + sloName := sdkacctest.RandStringFromCharSet(22, sdkacctest.CharSetAlphaNum) + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: checkResourceSloDestroy, + Steps: []resource.TestStep{ + { + ProtoV6ProviderFactories: acctest.Providers, + SkipFunc: versionutils.CheckIfVersionIsUnsupported(sloTimesliceMetricsMinVersion), + Config: fmt.Sprintf(` + provider "elasticstack" { + elasticsearch {} + kibana {} + } + + resource "elasticstack_elasticsearch_index" "my_index" { + name = "my-index" + deletion_protection = false + } + resource "elasticstack_kibana_slo" "test_slo" { + name = "%s" + description = "multiple mixed metrics" + timeslice_metric_indicator { + index = "my-index" + timestamp_field = "@timestamp" + metric { + metrics { + name = "A" + aggregation = "avg" + field = "bops" + } + metrics { + name = "B" + aggregation = "percentile" + field = "latency" + percentile = 99 + } + metrics { + name = "C" + aggregation = "doc_count" + } + equation = "A + B + C" + comparator = "GT" + threshold = 100 + } + } + budgeting_method = "timeslices" + objective { + target = 0.95 + timeslice_target = 0.95 + timeslice_window = "5m" + } + time_window { + duration = "7d" + type = "rolling" + } + depends_on = [elasticstack_elasticsearch_index.my_index] + } + `, sloName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.index", "my-index"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.timestamp_field", "@timestamp"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.name", "A"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.aggregation", "avg"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.0.field", "bops"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.1.name", "B"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.1.aggregation", "percentile"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.1.percentile", "99"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.2.name", "C"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.metrics.2.aggregation", "doc_count"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.equation", "A + B + C"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.comparator", "GT"), + resource.TestCheckResourceAttr("elasticstack_kibana_slo.test_slo", "timeslice_metric_indicator.0.metric.0.threshold", "100"), + ), + }, + }, + }) +} + func TestAccResourceSloErrors(t *testing.T) { multipleIndicatorsConfig := ` provider "elasticstack" { @@ -539,6 +831,23 @@ func getSLOConfig(vars sloVars) string { } } `, vars.name) + case "timeslice_metric_indicator": + indicator = fmt.Sprintf(` + timeslice_metric_indicator { + index = "my-index-%s" + timestamp_field = "@timestamp" + metric { + metrics { + name = "A" + aggregation = "sum" + field = "latency" + } + equation = "A" + comparator = "GT" + threshold = 100 + } + } + `, vars.name) } return indicator }