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
}