diff --git a/client/metric_conditions.go b/client/metric_conditions.go index cb2aa5e7..5454d80b 100644 --- a/client/metric_conditions.go +++ b/client/metric_conditions.go @@ -48,14 +48,17 @@ type Expression struct { } type SubAlertExpression struct { - Thresholds Thresholds `json:"thresholds"` - Operand string `json:"operand"` - IsNoData bool `json:"enable-no-data-alert,omitempty"` + Thresholds Thresholds `json:"thresholds"` + Operand string `json:"operand"` + IsNoData bool `json:"enable-no-data-alert,omitempty"` + NoDataDurationMs *int `json:"no-data-duration-ms,omitempty"` } type Thresholds struct { - Warning *float64 `json:"warning,omitempty"` - Critical *float64 `json:"critical,omitempty"` + Warning *float64 `json:"warning,omitempty"` + WarningDurationMs *int `json:"warning-duration-ms,omitempty"` + Critical *float64 `json:"critical,omitempty"` + CriticalDurationMs *int `json:"critical-duration-ms,omitempty"` } type DependencyMapOptions struct { diff --git a/docs/resources/alert.md b/docs/resources/alert.md index db1ec9b2..42219659 100644 --- a/docs/resources/alert.md +++ b/docs/resources/alert.md @@ -110,6 +110,7 @@ Optional: Optional: - `is_no_data` (Boolean) If true, a notification is sent when the alert query returns no data. If false, notifications aren't sent in this scenario. +- `no_data_duration_ms` (Number) No data must be seen for this duration before the status changes. - `operand` (String) Required when at least one threshold (Critical, Warning) is defined. Indicates whether the alert triggers when the value is above the threshold or below the threshold. - `thresholds` (Block List, Max: 1) Optional values defining the thresholds at which this alert transitions into Critical or Warning states. If a particular threshold is not specified, the alert never transitions into that state. (see [below for nested schema](#nestedblock--composite_alert--alert--expression--thresholds)) @@ -119,7 +120,9 @@ Optional: Optional: - `critical` (String) Defines the threshold for the alert to transition to a Critical (more severe) status. +- `critical_duration_ms` (Number) Critical threshold must be breached for this duration before the status changes. - `warning` (String) Defines the threshold for the alert to transition to a Warning (less severe) status. +- `warning_duration_ms` (Number) Critical threshold must be breached for this duration before the status changes. @@ -161,6 +164,7 @@ Optional: - `is_multi` (Boolean) When false, send a single notification whenever any number of group_by values exceeds the alert threshold. When true, send individual notifications for each distinct group_by value that exceeds the threshold. - `is_no_data` (Boolean) If true, a notification is sent when the alert query returns no data. If false, notifications aren't sent in this scenario. +- `no_data_duration_ms` (Number) No data must be seen for this duration before the status changes. - `operand` (String) Required when at least one threshold (Critical, Warning) is defined. Indicates whether the alert triggers when the value is above the threshold or below the threshold. - `thresholds` (Block List, Max: 1) Optional values defining the thresholds at which this alert transitions into Critical or Warning states. If a particular threshold is not specified, the alert never transitions into that state. (see [below for nested schema](#nestedblock--expression--thresholds)) @@ -170,7 +174,9 @@ Optional: Optional: - `critical` (String) Defines the threshold for the alert to transition to a Critical (more severe) status. +- `critical_duration_ms` (Number) Critical threshold must be breached for this duration before the status changes. - `warning` (String) Defines the threshold for the alert to transition to a Warning (less severe) status. +- `warning_duration_ms` (Number) Critical threshold must be breached for this duration before the status changes. diff --git a/docs/resources/metric_condition.md b/docs/resources/metric_condition.md index 74dc1079..a9f1ec5f 100644 --- a/docs/resources/metric_condition.md +++ b/docs/resources/metric_condition.md @@ -120,6 +120,7 @@ Optional: - `is_multi` (Boolean) When false, send a single notification whenever any number of group_by values exceeds the alert threshold. When true, send individual notifications for each distinct group_by value that exceeds the threshold. - `is_no_data` (Boolean) If true, a notification is sent when the alert query returns no data. If false, notifications aren't sent in this scenario. +- `no_data_duration_ms` (Number) No data must be seen for this duration before the status changes. - `operand` (String) Required when at least one threshold (Critical, Warning) is defined. Indicates whether the alert triggers when the value is above the threshold or below the threshold. - `thresholds` (Block List, Max: 1) Optional values defining the thresholds at which this alert transitions into Critical or Warning states. If a particular threshold is not specified, the alert never transitions into that state. (see [below for nested schema](#nestedblock--expression--thresholds)) @@ -129,7 +130,9 @@ Optional: Optional: - `critical` (String) Defines the threshold for the alert to transition to a Critical (more severe) status. +- `critical_duration_ms` (Number) Critical threshold must be breached for this duration before the status changes. - `warning` (String) Defines the threshold for the alert to transition to a Warning (less severe) status. +- `warning_duration_ms` (Number) Critical threshold must be breached for this duration before the status changes. diff --git a/lightstep/resource_alert.go b/lightstep/resource_alert.go index bff24a37..b94e9dcf 100644 --- a/lightstep/resource_alert.go +++ b/lightstep/resource_alert.go @@ -2,6 +2,7 @@ package lightstep import ( "fmt" + "github.com/lightstep/terraform-provider-lightstep/client" ) @@ -52,7 +53,7 @@ func getCompositeAlertFromUnifiedConditionResourceData(compositeAlertIn *client. return nil, err } - subAlerts = append(subAlerts, map[string]interface{}{ + subAlertExpressionMap := map[string]interface{}{ "name": subAlertIn.Name, "title": subAlertIn.Title, "expression": []map[string]interface{}{ @@ -63,7 +64,11 @@ func getCompositeAlertFromUnifiedConditionResourceData(compositeAlertIn *client. }, }, "query": queries, - }) + } + if subAlertIn.Expression.NoDataDurationMs != nil { + subAlertExpressionMap["no_data_duration_ms"] = subAlertIn.Expression.NoDataDurationMs + } + subAlerts = append(subAlerts, subAlertExpressionMap) } return []map[string][]map[string]interface{}{{ diff --git a/lightstep/resource_alert_test.go b/lightstep/resource_alert_test.go index bfb25178..52121791 100644 --- a/lightstep/resource_alert_test.go +++ b/lightstep/resource_alert_test.go @@ -62,13 +62,6 @@ resource "lightstep_alert" "test" { alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [ - { - key = "project_name" - value = "catlab" - } - ] } } ` @@ -103,13 +96,6 @@ resource "lightstep_alert" "test" { alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [ - { - key = "project_name" - value = "catlab" - } - ] } } ` @@ -206,13 +192,6 @@ EOT alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [ - { - key = "project_name" - value = "catlab" - } - ] } } `, uqlQuery) @@ -251,13 +230,6 @@ EOT alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [ - { - key = "project_name" - value = "catlab" - } - ] } } `, uqlQuery) @@ -282,10 +254,6 @@ EOT resource.TestCheckResourceAttr(resourceName, "name", "Too many requests"), resource.TestCheckResourceAttr(resourceName, "description", "A link to a playbook"), resource.TestCheckResourceAttr(resourceName, "query.0.query_string", uqlQuery+"\n"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "alerting_rule.*", map[string]string{ - "include_filters.0.key": "project_name", - "include_filters.0.value": "catlab", - }), resource.TestCheckResourceAttr(resourceName, "expression.0.is_no_data", "true"), ), }, @@ -553,11 +521,6 @@ EOT alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [{ - key = "project_name" - value = "catlab" - }] } } `, uqlQuery) @@ -706,13 +669,6 @@ resource "lightstep_alert" "errors" { alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [ - { - key = "project_name" - value = "catlab" - } - ] } } ` @@ -769,13 +725,6 @@ resource "lightstep_alert" "test" { alerting_rule { id = lightstep_slack_destination.slack.id update_interval = "1h" - - include_filters = [ - { - key = "project_name" - value = "catlab" - } - ] } } ` @@ -1172,3 +1121,53 @@ resource "lightstep_alert" "composite_diff_test" { }, }) } + +func TestAccAlertWithThresholdDurations(t *testing.T) { + var condition client.UnifiedCondition + + conditionConfig := ` +resource "lightstep_alert" "test" { + project_name = "` + testProject + `" + name = "Too many requests" + + expression { + is_multi = true + is_no_data = true + no_data_duration_ms = 60000 + operand = "above" + thresholds { + critical = 10 + critical_duration_ms = 180000 + warning = 5 + warning_duration_ms = 120000 + } + } + + query { + query_name = "a" + hidden = false + display = "line" + query_string = "metric requests | rate 1h | filter service_name == frontend | group_by [method], mean" + } +} +` + + resourceName := "lightstep_alert.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccMetricConditionDestroy, + Steps: []resource.TestStep{ + { + Config: conditionConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckLightstepAlertExists(resourceName, &condition), + resource.TestCheckResourceAttr(resourceName, "name", "Too many requests"), + resource.TestCheckResourceAttr(resourceName, "expression.0.no_data_duration_ms", "60000"), + resource.TestCheckResourceAttr(resourceName, "expression.0.thresholds.0.warning_duration_ms", "120000"), + resource.TestCheckResourceAttr(resourceName, "expression.0.thresholds.0.critical_duration_ms", "180000"), + ), + }, + }, + }) +} diff --git a/lightstep/resource_metric_condition.go b/lightstep/resource_metric_condition.go index 9b19b288..3e6e7d69 100644 --- a/lightstep/resource_metric_condition.go +++ b/lightstep/resource_metric_condition.go @@ -338,11 +338,21 @@ func getThresholdSchemaMap() map[string]*schema.Schema { Optional: true, Description: "Defines the threshold for the alert to transition to a Critical (more severe) status.", }, + "critical_duration_ms": { + Type: schema.TypeInt, + Optional: true, + Description: "Critical threshold must be breached for this duration before the status changes.", + }, "warning": { Type: schema.TypeString, Optional: true, Description: "Defines the threshold for the alert to transition to a Warning (less severe) status.", }, + "warning_duration_ms": { + Type: schema.TypeInt, + Optional: true, + Description: "Critical threshold must be breached for this duration before the status changes.", + }, } } @@ -422,6 +432,11 @@ func getCompositeSubAlertExpressionResource() *schema.Resource { Default: false, Description: "If true, a notification is sent when the alert query returns no data. If false, notifications aren't sent in this scenario.", }, + "no_data_duration_ms": { + Type: schema.TypeInt, + Optional: true, + Description: "No data must be seen for this duration before the status changes.", + }, "operand": { Type: schema.TypeString, Optional: true, @@ -524,7 +539,8 @@ func (p *resourceUnifiedConditionImp) resourceUnifiedConditionRead(ctx context.C return diag.FromErr(fmt.Errorf("failed to translate resource attributes: %v", err)) } - cond, err := c.GetUnifiedCondition(ctx, d.Get("project_name").(string), d.Id()) + projectName := d.Get("project_name").(string) + cond, err := c.GetUnifiedCondition(ctx, projectName, d.Id()) if err != nil { apiErr, ok := err.(client.APIResponseCarrier) if !ok { @@ -539,7 +555,6 @@ func (p *resourceUnifiedConditionImp) resourceUnifiedConditionRead(ctx context.C return diag.FromErr(fmt.Errorf("failed to get metric condition: %v", apiErr)) } - projectName := d.Get("project_name").(string) legacy, err := metricConditionHasEquivalentLegacyQueries(ctx, c, projectName, prevAttrs, &cond.Attributes) if err != nil { return diag.FromErr(fmt.Errorf("failed to compare legacy queries: %v", err)) @@ -548,7 +563,8 @@ func (p *resourceUnifiedConditionImp) resourceUnifiedConditionRead(ctx context.C cond.Attributes.Queries = prevAttrs.Queries } - if err := setResourceDataFromUnifiedCondition(d.Get("project_name").(string), *cond, d, p.conditionSchemaType); err != nil { + err = setResourceDataFromUnifiedCondition(projectName, *cond, d, p.conditionSchemaType) + if err != nil { return diag.FromErr(fmt.Errorf("failed to set metric condition from API response to terraform state: %v", err)) } @@ -678,11 +694,21 @@ func buildSubAlertExpression(singleExpression map[string]interface{}) (*client.S return nil, err } - return &client.SubAlertExpression{ + e := &client.SubAlertExpression{ IsNoData: singleExpression["is_no_data"].(bool), Operand: singleExpression["operand"].(string), Thresholds: thresholds, - }, nil + } + + noDataDuration := singleExpression["no_data_duration_ms"] + if noDataDuration != nil && noDataDuration != "" && noDataDuration != 0 { + d, ok := noDataDuration.(int) + if !ok { + return e, err + } + e.NoDataDurationMs = &d + } + return e, nil } func buildAlertingRules(alertingRulesIn *schema.Set) ([]client.AlertingRule, error) { @@ -1014,6 +1040,24 @@ func buildThresholds(singleExpression map[string]interface{}) (client.Thresholds t.Warning = &w } + criticalDuration := thresholdsObj["critical_duration_ms"] + if criticalDuration != nil && criticalDuration != "" && criticalDuration != 0 { + d, ok := criticalDuration.(int) + if !ok { + return t, fmt.Errorf("unexpected format for critical_duration_ms") + } + t.CriticalDurationMs = &d + } + + warningDuration := thresholdsObj["warning_duration_ms"] + if warningDuration != nil && warningDuration != "" && criticalDuration != 0 { + d, ok := warningDuration.(int) + if !ok { + return t, fmt.Errorf("unexpected format for warning_duration_ms") + } + t.WarningDurationMs = &d + } + return t, nil } @@ -1231,14 +1275,17 @@ func setResourceDataFromUnifiedCondition(project string, c client.UnifiedConditi } if c.Attributes.Expression != nil { - if err := d.Set("expression", []map[string]interface{}{ - { - "is_multi": c.Attributes.Expression.IsMulti, - "is_no_data": c.Attributes.Expression.IsNoData, - "operand": c.Attributes.Expression.Operand, - "thresholds": buildUntypedThresholds(c.Attributes.Expression.Thresholds), - }, - }); err != nil { + expressionMap := map[string]interface{}{ + "is_multi": c.Attributes.Expression.IsMulti, + "is_no_data": c.Attributes.Expression.IsNoData, + "operand": c.Attributes.Expression.Operand, + "thresholds": buildUntypedThresholds(c.Attributes.Expression.Thresholds), + } + if c.Attributes.Expression.NoDataDurationMs != nil { + expressionMap["no_data_duration_ms"] = c.Attributes.Expression.NoDataDurationMs + } + expressionSlice := []map[string]interface{}{expressionMap} + if err := d.Set("expression", expressionSlice); err != nil { return fmt.Errorf("unable to set expression resource field: %v", err) } } @@ -1314,6 +1361,13 @@ func buildUntypedThresholds(thresholds client.Thresholds) []map[string]interface if thresholds.Warning != nil { outputMap["warning"] = strconv.FormatFloat(*thresholds.Warning, 'f', -1, 64) } + + if thresholds.CriticalDurationMs != nil { + outputMap["critical_duration_ms"] = thresholds.CriticalDurationMs + } + if thresholds.WarningDurationMs != nil { + outputMap["warning_duration_ms"] = thresholds.WarningDurationMs + } return []map[string]interface{}{ outputMap, }