From dacb37803e4d684001fa1fde2f0f0d9d5a415861 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Thu, 15 Aug 2019 10:24:42 -0500 Subject: [PATCH] Adds Service Level Objective (SLO) support (#263) * add SLO support * fix docs * fix docs * small tweaks to formatting * PR review fixes * add clarifying sentence * fix test * fix provider to Optional fields make syntax backwards compatible suppress *_display changes * because of https://github.com/hashicorp/terraform/issues/6215/ move from TypeMap to TypeList and validate * add validation to query options * Revert "add validation to query options" This reverts commit 6338ed2b4594fa083eab44bd8d26fafd7ccdcb69. * fix docs address possible difference in state value for thresholds * apply same logic to query since it is now TypeList * fix * ValueType casts as []interface{} for TypeList * fix dangling character --- datadog/provider.go | 1 + ...esource_datadog_service_level_objective.go | 503 ++++++++++++++++++ ...ce_datadog_service_level_objective_test.go | 197 +++++++ website/datadog.erb | 3 + .../r/service_level_objective.html.markdown | 122 +++++ 5 files changed, 826 insertions(+) create mode 100644 datadog/resource_datadog_service_level_objective.go create mode 100644 datadog/resource_datadog_service_level_objective_test.go create mode 100644 website/docs/r/service_level_objective.html.markdown diff --git a/datadog/provider.go b/datadog/provider.go index ebb932286..c8a2ebec3 100644 --- a/datadog/provider.go +++ b/datadog/provider.go @@ -44,6 +44,7 @@ func Provider() terraform.ResourceProvider { "datadog_integration_aws": resourceDatadogIntegrationAws(), "datadog_integration_pagerduty": resourceDatadogIntegrationPagerduty(), "datadog_integration_pagerduty_service_object": resourceDatadogIntegrationPagerdutySO(), + "datadog_service_level_objective": resourceDatadogServiceLevelObjective(), }, ConfigureFunc: providerConfigure, diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go new file mode 100644 index 000000000..08c17b665 --- /dev/null +++ b/datadog/resource_datadog_service_level_objective.go @@ -0,0 +1,503 @@ +package datadog + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/zorkian/go-datadog-api" +) + +func resourceDatadogServiceLevelObjective() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogServiceLevelObjectiveCreate, + Read: resourceDatadogServiceLevelObjectiveRead, + Update: resourceDatadogServiceLevelObjectiveUpdate, + Delete: resourceDatadogServiceLevelObjectiveDelete, + Exists: resourceDatadogServiceLevelObjectiveExists, + Importer: &schema.ResourceImporter{ + State: resourceDatadogServiceLevelObjectiveImport, + }, + + Schema: map[string]*schema.Schema{ + // Common + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) + }, + }, + "tags": { + // we use TypeSet to represent tags, paradoxically to be able to maintain them ordered; + // we order them explicitly in the read/create/update methods of this resource and using + // TypeSet makes Terraform ignore differences in order when creating a plan + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "thresholds": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "timeframe": { + Type: schema.TypeString, + Required: true, + }, + "target": { + Type: schema.TypeFloat, + Required: true, + DiffSuppressFunc: suppressDataDogFloatIntDiff, + }, + "target_display": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppressDataDogSLODisplayValueDiff, + }, + "warning": { + Type: schema.TypeFloat, + Optional: true, + DiffSuppressFunc: suppressDataDogFloatIntDiff, + }, + "warning_display": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: suppressDataDogSLODisplayValueDiff, + }, + }, + }, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: ValidateServiceLevelObjectiveTypeString, + }, + + // Metric-Based SLO + "query": { + // we use TypeList here because of https://github.com/hashicorp/terraform/issues/6215/ + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ConflictsWith: []string{"monitor_ids", "monitor_search", "groups"}, + Description: "The metric query of good / total events", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "numerator": { + Type: schema.TypeString, + Required: true, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) + }, + }, + "denominator": { + Type: schema.TypeString, + Required: true, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) + }, + }, + }, + }, + }, + + // Monitor-Based SLO + "monitor_ids": { + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"query", "monitor_search"}, + Description: "A static set of monitor IDs to use as part of the SLO", + Elem: &schema.Schema{Type: schema.TypeInt, MinItems: 1}, + }, + "monitor_search": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"query", "monitor_ids"}, + Description: "A dynamic search on creation for the SLO", + }, + "groups": { + Type: schema.TypeSet, + Optional: true, + Description: "A static set of groups to filter monitor-based SLOs", + ConflictsWith: []string{"query"}, + Elem: &schema.Schema{Type: schema.TypeString, MinItems: 1}, + }, + }, + } +} + +// ValidateServiceLevelObjectiveTypeString is a ValidateFunc that ensures the SLO is of one of the supported types +func ValidateServiceLevelObjectiveTypeString(v interface{}, k string) (ws []string, errors []error) { + switch v.(string) { + case datadog.ServiceLevelObjectiveTypeMonitor: + break + case datadog.ServiceLevelObjectiveTypeMetric: + break + default: + errors = append(errors, fmt.Errorf("invalid type %s specified for SLO", v.(string))) + } + return +} + +func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLevelObjective { + + slo := datadog.ServiceLevelObjective{ + Type: datadog.String(d.Get("type").(string)), + Name: datadog.String(d.Get("name").(string)), + } + + if attr, ok := d.GetOk("description"); ok { + slo.Description = datadog.String(attr.(string)) + } + + if attr, ok := d.GetOk("tags"); ok { + tags := make([]string, 0) + for _, s := range attr.(*schema.Set).List() { + tags = append(tags, s.(string)) + } + // sort to make them determinate + if len(tags) > 0 { + sort.Strings(tags) + slo.Tags = tags + } + } + + if _, ok := d.GetOk("thresholds"); ok { + numThresholds := d.Get("thresholds.#").(int) + sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) + for i := 0; i < numThresholds; i++ { + prefix := fmt.Sprintf("thresholds.%d.", i) + t := datadog.ServiceLevelObjectiveThreshold{} + + if tf, ok := d.GetOk(prefix + "timeframe"); ok { + t.TimeFrame = datadog.String(tf.(string)) + } + + if targetValue, ok := d.GetOk(prefix + "target"); ok { + if f, ok := floatOk(targetValue); ok { + t.Target = datadog.Float64(f) + } + } + + if warningValue, ok := d.GetOk(prefix + "warning"); ok { + if f, ok := floatOk(warningValue); ok { + t.Warning = datadog.Float64(f) + } + } + + if targetDisplayValue, ok := d.GetOk(prefix + "target_display"); ok { + if s, ok := targetDisplayValue.(string); ok && strings.TrimSpace(s) != "" { + t.TargetDisplay = datadog.String(strings.TrimSpace(targetDisplayValue.(string))) + } + } + + if warningDisplayValue, ok := d.GetOk(prefix + "warning_display"); ok { + if s, ok := warningDisplayValue.(string); ok && strings.TrimSpace(s) != "" { + t.WarningDisplay = datadog.String(strings.TrimSpace(warningDisplayValue.(string))) + } + } + sloThresholds = append(sloThresholds, &t) + } + sort.Sort(sloThresholds) + slo.Thresholds = sloThresholds + } + + switch d.Get("type").(string) { + case datadog.ServiceLevelObjectiveTypeMonitor: + // add monitor components + if attr, ok := d.GetOk("monitor_ids"); ok { + monitorIDs := make([]int, 0) + for _, s := range attr.(*schema.Set).List() { + monitorIDs = append(monitorIDs, s.(int)) + } + if len(monitorIDs) > 0 { + sort.Ints(monitorIDs) + slo.MonitorIDs = monitorIDs + } + } + if attr, ok := d.GetOk("monitor_search"); ok { + if len(attr.(string)) > 0 { + slo.MonitorSearch = datadog.String(attr.(string)) + } + } + if attr, ok := d.GetOk("groups"); ok { + groups := make([]string, 0) + for _, s := range attr.(*schema.Set).List() { + groups = append(groups, s.(string)) + } + if len(groups) > 0 { + sort.Strings(groups) + slo.Groups = groups + } + } + default: + // query type + if _, ok := d.GetOk("query.0"); ok { + slo.Query = &datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String(d.Get("query.0.numerator").(string)), + Denominator: datadog.String(d.Get("query.0.denominator").(string)), + } + } + } + + return &slo +} + +func floatOk(val interface{}) (float64, bool) { + switch val.(type) { + case float64: + return val.(float64), true + case *float64: + return *(val.(*float64)), true + case string: + f, err := strconv.ParseFloat(val.(string), 64) + if err == nil { + return f, true + } + case *string: + f, err := strconv.ParseFloat(*(val.(*string)), 64) + if err == nil { + return f, true + } + default: + return 0, false + } + return 0, false +} + +func resourceDatadogServiceLevelObjectiveCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + slo := buildServiceLevelObjectiveStruct(d) + slo, err := client.CreateServiceLevelObjective(slo) + if err != nil { + return fmt.Errorf("error creating service level objective: %s", err.Error()) + } + + d.SetId(slo.GetID()) + + return resourceDatadogServiceLevelObjectiveRead(d, meta) +} + +func resourceDatadogServiceLevelObjectiveExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { + // Exists - This is called to verify a resource still exists. It is called prior to Read, + // and lowers the burden of Read to be able to assume the resource exists. + client := meta.(*datadog.Client) + + if _, err := client.GetServiceLevelObjective(d.Id()); err != nil { + errStr := strings.ToLower(err.Error()) + if strings.Contains(errStr, "not found") || strings.Contains(errStr, "no slo specified") { + return false, nil + } + return false, err + } + + return true, nil +} + +func resourceDatadogServiceLevelObjectiveRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + slo, err := client.GetServiceLevelObjective(d.Id()) + if err != nil { + return err + } + + thresholds := make([]map[string]interface{}, 0) + sort.Sort(slo.Thresholds) + for _, threshold := range slo.Thresholds { + t := map[string]interface{}{ + "timeframe": threshold.GetTimeFrame(), + "target": threshold.GetTarget(), + "warning": threshold.GetWarning(), + } + if targetDisplay, ok := threshold.GetTargetDisplayOk(); ok { + t["target_display"] = targetDisplay + } + if warningDisplay, ok := threshold.GetWarningDisplayOk(); ok { + t["warning_display"] = warningDisplay + } + thresholds = append(thresholds, t) + } + + tags := make([]string, 0) + for _, s := range slo.Tags { + tags = append(tags, s) + } + sort.Strings(tags) + + d.Set("name", slo.GetName()) + d.Set("description", slo.GetDescription()) + d.Set("type", slo.GetType()) + d.Set("tags", tags) + d.Set("thresholds", thresholds) + switch slo.GetType() { + case datadog.ServiceLevelObjectiveTypeMonitor: + // monitor type + if len(slo.MonitorIDs) > 0 { + sort.Ints(slo.MonitorIDs) + d.Set("monitor_ids", slo.MonitorIDs) + } + if ms, ok := slo.GetMonitorSearchOk(); ok { + d.Set("monitor_search", ms) + } + sort.Strings(slo.Groups) + d.Set("groups", slo.Groups) + default: + // metric type + query := make(map[string]interface{}) + q := slo.GetQuery() + query["numerator"] = q.GetNumerator() + query["denominator"] = q.GetDenominator() + d.Set("query", []map[string]interface{}{query}) + } + + return nil +} + +func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + slo := &datadog.ServiceLevelObjective{ + ID: datadog.String(d.Id()), + } + + if attr, ok := d.GetOk("name"); ok { + slo.SetName(attr.(string)) + } + + if attr, ok := d.GetOk("description"); ok { + slo.SetDescription(attr.(string)) + } + + if attr, ok := d.GetOk("type"); ok { + slo.SetType(attr.(string)) + } + + switch slo.GetType() { + case datadog.ServiceLevelObjectiveTypeMonitor: + // monitor type + if attr, ok := d.GetOk("monitor_ids"); ok { + s := make([]int, 0) + for _, v := range attr.(*schema.Set).List() { + s = append(s, v.(int)) + } + sort.Ints(s) + slo.MonitorIDs = s + } + if attr, ok := d.GetOk("monitor_search"); ok { + slo.SetMonitorSearch(attr.(string)) + } + if attr, ok := d.GetOk("groups"); ok { + s := make([]string, 0) + for _, v := range attr.(*schema.Set).List() { + s = append(s, v.(string)) + } + sort.Strings(s) + slo.Groups = s + } + default: + // metric type + if attr, ok := d.GetOk("query"); ok { + queries := make([]map[string]interface{}, 0) + raw := attr.([]interface{}) + for _, rawQuery := range raw { + if query, ok := rawQuery.(map[string]interface{}); ok { + queries = append(queries, query) + } + } + if len(queries) >= 1 { + // only use the first defined query + slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String(queries[0]["numerator"].(string)), + Denominator: datadog.String(queries[0]["denominator"].(string)), + }) + } + } + } + + if attr, ok := d.GetOk("tags"); ok { + s := make([]string, 0) + for _, v := range attr.(*schema.Set).List() { + s = append(s, v.(string)) + } + sort.Strings(s) + slo.Tags = s + } + + if attr, ok := d.GetOk("thresholds"); ok { + sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) + thresholds := make([]map[string]interface{}, 0) + raw := attr.([]interface{}) + for _, rawThreshold := range raw { + if threshold, ok := rawThreshold.(map[string]interface{}); ok { + thresholds = append(thresholds, threshold) + } + } + for _, threshold := range thresholds { + t := datadog.ServiceLevelObjectiveThreshold{ + TimeFrame: datadog.String(threshold["timeframe"].(string)), + Target: datadog.Float64(threshold["target"].(float64)), + } + if warningValueRaw, ok := threshold["warning"]; ok { + t.Warning = datadog.Float64(warningValueRaw.(float64)) + } + // display settings + if targetDisplay, ok := threshold["target_display"]; ok { + t.TargetDisplay = datadog.String(targetDisplay.(string)) + } + if warningDisplay, ok := threshold["warning_display"]; ok { + t.WarningDisplay = datadog.String(warningDisplay.(string)) + } + sloThresholds = append(sloThresholds, &t) + } + if len(sloThresholds) > 0 { + sort.Sort(sloThresholds) + slo.Thresholds = sloThresholds + } + } + + if _, err := client.UpdateServiceLevelObjective(slo); err != nil { + return fmt.Errorf("error updating service level objective: %s", err.Error()) + } + + return resourceDatadogServiceLevelObjectiveRead(d, meta) +} + +func resourceDatadogServiceLevelObjectiveDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + return client.DeleteServiceLevelObjective(d.Id()) +} + +func resourceDatadogServiceLevelObjectiveImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if err := resourceDatadogServiceLevelObjectiveRead(d, meta); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} + +// Ignore any diff that results from the mix of *_display string values from the +// DataDog API. +func suppressDataDogSLODisplayValueDiff(k, old, new string, d *schema.ResourceData) bool { + sloType := d.Get("type") + if sloType == datadog.ServiceLevelObjectiveTypeMonitor { + // always suppress monitor type, this is controlled via API. + return true + } + + // metric type otherwise + if old == "" || new == "" { + // always suppress if not specified + return true + } + + return suppressDataDogFloatIntDiff(k, old, new, d) +} diff --git a/datadog/resource_datadog_service_level_objective_test.go b/datadog/resource_datadog_service_level_objective_test.go new file mode 100644 index 000000000..c466aeb3a --- /dev/null +++ b/datadog/resource_datadog_service_level_objective_test.go @@ -0,0 +1,197 @@ +package datadog + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/zorkian/go-datadog-api" +) + +// config +const testAccCheckDatadogServiceLevelObjectiveConfig = ` +resource "datadog_service_level_objective" "foo" { + name = "name for metric SLO foo" + type = "metric" + description = "some description about foo SLO" + query { + numerator = "sum:my.metric{type:good}.as_count()" + denominator = "sum:my.metric{*}.as_count()" + } + + thresholds { + timeframe = "7d" + target = 99.5 + warning = 99.8 + } + + thresholds { + timeframe = "30d" + target = 99 + } + + tags = ["foo:bar", "baz"] +} +` + +const testAccCheckDatadogServiceLevelObjectiveConfigUpdated = ` +resource "datadog_service_level_objective" "foo" { + name = "updated name for metric SLO foo" + type = "metric" + description = "some updated description about foo SLO" + query { + numerator = "sum:my.metric{type:good}.as_count()" + denominator = "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()" + } + + thresholds { + timeframe = "7d" + target = 99.5 + warning = 99.8 + } + + thresholds { + timeframe = "30d" + target = 98 + } + + tags = ["foo:bar", "baz"] +} +` + +// tests + +func TestAccDatadogServiceLevelObjective_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogServiceLevelObjectiveDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogServiceLevelObjectiveConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogServiceLevelObjectiveExists("datadog_service_level_objective.foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "name", "name for metric SLO foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "description", "some description about foo SLO"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "type", "metric"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.0.numerator", "sum:my.metric{type:good}.as_count()"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.0.denominator", "sum:my.metric{*}.as_count()"), + // Thresholds are a TypeList, that are sorted by timeframe alphabetically. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.timeframe", "7d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.target", "99.5"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.warning", "99.8"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.timeframe", "30d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.target", "99"), + // Tags are a TypeSet => use a weird way to access members by their hash + // TF TypeSet is internally represented as a map that maps computed hashes + // to actual values. Since the hashes are always the same for one value, + // this is the way to get them. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.2644851163", "baz"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.1750285118", "foo:bar"), + ), + }, + { + Config: testAccCheckDatadogServiceLevelObjectiveConfigUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogServiceLevelObjectiveExists("datadog_service_level_objective.foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "name", "updated name for metric SLO foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "description", "some updated description about foo SLO"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "type", "metric"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.0.numerator", "sum:my.metric{type:good}.as_count()"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.0.denominator", "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()"), + // Thresholds are a TypeList, that are sorted by timeframe alphabetically. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.timeframe", "7d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.target", "99.5"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.warning", "99.8"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.timeframe", "30d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.target", "98"), + // Tags are a TypeSet => use a weird way to access members by their hash + // TF TypeSet is internally represented as a map that maps computed hashes + // to actual values. Since the hashes are always the same for one value, + // this is the way to get them. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.2644851163", "baz"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.1750285118", "foo:bar"), + ), + }, + }, + }) +} + +// helpers + +func destroyServiceLevelObjectiveHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + if r.Primary.ID != "" { + if _, err := client.GetServiceLevelObjective(r.Primary.ID); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + continue + } + return fmt.Errorf("Received an error retrieving service level objective %s", err) + } + return fmt.Errorf("Service Level Objective still exists") + } + } + return nil +} + +func existsServiceLevelObjectiveHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + if _, err := client.GetServiceLevelObjective(r.Primary.ID); err != nil { + return fmt.Errorf("Received an error retrieving service level objective %s", err) + } + } + return nil +} + +func testAccCheckDatadogServiceLevelObjectiveDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := destroyServiceLevelObjectiveHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogServiceLevelObjectiveExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + if err := existsHelper(s, client); err != nil { + return err + } + return nil + } +} diff --git a/website/datadog.erb b/website/datadog.erb index 9624ad53a..a3a2d405e 100644 --- a/website/datadog.erb +++ b/website/datadog.erb @@ -40,6 +40,9 @@ > datadog_screenboard + > + datadog_service_level_objective + > datadog_synthetics diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown new file mode 100644 index 000000000..37c79563f --- /dev/null +++ b/website/docs/r/service_level_objective.html.markdown @@ -0,0 +1,122 @@ +--- +layout: "datadog" +page_title: "Datadog: datadog_service_level_objective" +sidebar_current: "docs-datadog-resource-service_level_objective" +description: |- + Provides a Datadog service level objective resource. This can be used to create and manage service level objectives. +--- + +# datadog_service_level_objective + +Provides a Datadog service level objective resource. This can be used to create and manage Datadog service level objectives. + +## Example Usage + +### Metric-Based SLO +```hcl +# Create a new Datadog service level objective +resource "datadog_service_level_objective" "foo" { + name = "Example Metric SLO" + type = "metric" + description = "My custom metric SLO" + query { + numerator = "sum:my.custom.count.metric{type:good_events}.as_count()" + denominator = "sum:my.custom.count.metric{*}.as_count()" + } + + thresholds { + timeframe = "7d" + target = 99.9 + warning = 99.99 + target_display = "99.900" + warning_display = "99.990" + } + + thresholds { + timeframe = "30d" + target = 99.9 + warning = 99.99 + target_display = "99.900" + warning_display = "99.990" + } + + tags = ["foo:bar", "baz"] +} +``` + +### Monitor-Based SLO +```hcl +# Create a new Datadog service level objective +resource "datadog_service_level_objective" "bar" { + name = "Example Monitor SLO" + type = "monitor" + description = "My custom monitor SLO" + monitor_ids = [1, 2, 3] + + thresholds { + timeframe = "7d" + target = 99.9 + warning = 99.99 + } + + thresholds { + timeframe = "30d" + target = 99.9 + warning = 99.99 + } + + tags = ["foo:bar", "baz"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Required) The type of the service level objective. The mapping from these types to the types found in the Datadog Web UI can be found in the Datadog API [documentation](https://docs.datadoghq.com/api/?lang=python#create-a-service-level-objective) page. Available options to choose from are: + * `metric` + * `monitor` +* `name` - (Required) Name of Datadog service level objective +* `description` - (Optional) A description of this service level objective. +* `tags` (Optional) A list of tags to associate with your service level objective. This can help you categorize and filter service level objectives in the service level objectives page of the UI. Note: it's not currently possible to filter by these tags when querying via the API +* `thresholds` - (Required) - A list of thresholds and targets that define the service level objectives from the provided SLIs. + * `timeframe` (Required) - the time frame for the objective. The mapping from these types to the types found in the Datadog Web UI can be found in the Datadog API [documentation](https://docs.datadoghq.com/api/?lang=python#create-a-service-level-objective) page. Available options to choose from are: + * `7d` + * `30d` + * `90d` + * `target` - (Required) the objective's target `[0,100]` + * `target_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. + * `warning` - (Optional) the objective's warning value `[0,100]`. This must be `> target` value. + * `warning_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. + +The following options are specific to the `type` of service level objective: +* `metric` type SLOs: + * `query` - (Required) The metric query configuration to use for the SLI. This is a dictionary and requires both the `numerator` and `denominator` fields which should be `count` metrics using the `sum` aggregator. + * `numerator` - (Required) the sum of all the `good` events + * `denominator` - (Required) the sum of the `total` events + * Example Usage: +```hcl +query { + numerator = "sum:my.custom.count.metric{type:good}.as_count()" + denominator = "sum:my.custom.count.metric{*}.as_count()" +} +``` +* `monitor` type SLOs: + * `monitor_ids` - (Optional) A list of numeric monitor IDs for which to use as SLIs. Their tags will be auto-imported into `monitor_tags` field in the API resource. At least 1 of `monitor_ids` or `monitor_search` must be provided. + * `monitor_search` - (Optional) The monitor query search used on the monitor search API to add monitor_ids by searching. Their tags will be auto-imported into `monitor_tags` field in the API resource. At least 1 of `monitor_ids` or `monitor_search` must be provided. + * `groups` - (Optional) A custom set of groups from the monitor(s) for which to use as the SLI instead of all the groups. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - ID of the Datadog service level objective + +## Import + +Service Level Objectives can be imported using their string ID, e.g. + +``` +$ terraform import datadog_service_level_objective.12345678901234567890123456789012 "baz" +```