From 1b84048aeff2a25ebb3df3797176b4d1bb19712c Mon Sep 17 00:00:00 2001 From: Vincenzo Prignano Date: Wed, 3 Jun 2015 15:01:22 -0700 Subject: [PATCH 1/6] provider/datadog: Initial commit --- builtin/bins/provider-datadog/main.go | 12 + builtin/bins/provider-datadog/main_test.go | 1 + builtin/providers/datadog/provider.go | 33 +++ .../datadog/resource_datadog_monitor.go | 235 ++++++++++++++++++ 4 files changed, 281 insertions(+) create mode 100644 builtin/bins/provider-datadog/main.go create mode 100644 builtin/bins/provider-datadog/main_test.go create mode 100644 builtin/providers/datadog/provider.go create mode 100644 builtin/providers/datadog/resource_datadog_monitor.go diff --git a/builtin/bins/provider-datadog/main.go b/builtin/bins/provider-datadog/main.go new file mode 100644 index 000000000..05a9f31a9 --- /dev/null +++ b/builtin/bins/provider-datadog/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/datadog" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: datadog.Provider, + }) +} diff --git a/builtin/bins/provider-datadog/main_test.go b/builtin/bins/provider-datadog/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-datadog/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/datadog/provider.go b/builtin/providers/datadog/provider.go new file mode 100644 index 000000000..d66729d7f --- /dev/null +++ b/builtin/providers/datadog/provider.go @@ -0,0 +1,33 @@ +package datadog + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "api_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("DATADOG_API_KEY", nil), + }, + "app_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("DATADOG_APP_KEY", nil), + }, + }, + ResourcesMap: map[string]*schema.Resource{ + "datadog_monitor_metric": datadogMonitorResource(), + }, + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(rd *schema.ResourceData) (interface{}, error) { + apiKey := rd.Get("api_key").(string) + appKey := rd.Get("app_key").(string) + return map[string]string{"api_key": apiKey, "app_key": appKey}, nil +} diff --git a/builtin/providers/datadog/resource_datadog_monitor.go b/builtin/providers/datadog/resource_datadog_monitor.go new file mode 100644 index 000000000..85f4274ff --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_monitor.go @@ -0,0 +1,235 @@ +package datadog + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +const ( + monitorEndpoint = "https://app.datadoghq.com/api/v1/monitor" +) + +func datadogMonitorResource() *schema.Resource { + return &schema.Resource{ + Create: resourceMonitorCreate, + Read: resourceMonitorRead, + Update: resourceMonitorUpdate, + Delete: resourceMonitorDelete, + Exists: resourceMonitorExists, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + // Metric and Monitor settings + "metric": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "metric_tags": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "*", + }, + "time_aggr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "time_window": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "space_aggr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "operator": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "message": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + // Alert Settings + "warning": &schema.Schema{ + Type: schema.TypeMap, + Required: true, + }, + "critical": &schema.Schema{ + Type: schema.TypeMap, + Required: true, + }, + + // Additional Settings + "notify_no_data": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "no_data_timeframe": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + }, + } +} + +func getIDFromResponse(h *http.Response) (string, error) { + body, err := ioutil.ReadAll(h.Body) + if err != nil { + return "", err + } + h.Body.Close() + log.Println(h) + log.Println(string(body)) + v := map[string]interface{}{} + err = json.Unmarshal(body, &v) + if err != nil { + return "", err + } + if id, ok := v["id"]; ok { + return strconv.Itoa(int(id.(float64))), nil + } + return "", fmt.Errorf("error getting ID from response %s", h.Status) +} + +func marshalMetric(d *schema.ResourceData, typeStr string) ([]byte, error) { + name := d.Get("name").(string) + message := d.Get("message").(string) + timeAggr := d.Get("time_aggr").(string) + timeWindow := d.Get("time_window").(string) + spaceAggr := d.Get("space_aggr").(string) + metric := d.Get("metric").(string) + tags := d.Get("metric_tags").(string) + operator := d.Get("operator").(string) + query := fmt.Sprintf("%s(%s):%s:%s{%s} %s %s", timeAggr, timeWindow, spaceAggr, metric, tags, operator, d.Get(fmt.Sprintf("%s.threshold", typeStr))) + + log.Println(query) + m := map[string]interface{}{ + "type": "metric alert", + "query": query, + "name": fmt.Sprintf("[%s] %s", typeStr, name), + "message": fmt.Sprintf("%s %s", message, d.Get(fmt.Sprintf("%s.notify", typeStr))), + "options": map[string]interface{}{ + "notify_no_data": d.Get("notify_no_data").(bool), + "no_data_timeframe": d.Get("no_data_timeframe").(int), + }, + } + return json.Marshal(m) +} + +func authSuffix(meta interface{}) string { + m := meta.(map[string]string) + return fmt.Sprintf("?api_key=%s&application_key=%s", m["api_key"], m["app_key"]) +} + +func resourceMonitorCreate(d *schema.ResourceData, meta interface{}) error { + warningBody, err := marshalMetric(d, "warning") + if err != nil { + return err + } + criticalBody, err := marshalMetric(d, "critical") + if err != nil { + return err + } + + resW, err := http.Post(fmt.Sprintf("%s%s", monitorEndpoint, authSuffix(meta)), "application/json", bytes.NewReader(warningBody)) + if err != nil { + return fmt.Errorf("error creating warning: %s", err.Error()) + } + + resC, err := http.Post(fmt.Sprintf("%s%s", monitorEndpoint, authSuffix(meta)), "application/json", bytes.NewReader(criticalBody)) + if err != nil { + return fmt.Errorf("error creating critical: %s", err.Error()) + } + + warningMonitorID, err := getIDFromResponse(resW) + if err != nil { + return err + } + criticalMonitorID, err := getIDFromResponse(resC) + if err != nil { + return err + } + + d.SetId(fmt.Sprintf("%s__%s", warningMonitorID, criticalMonitorID)) + + return nil +} + +func resourceMonitorDelete(d *schema.ResourceData, meta interface{}) (e error) { + for _, v := range strings.Split(d.Id(), "__") { + client := http.Client{} + req, _ := http.NewRequest("DELETE", fmt.Sprintf("%s/%s%s", monitorEndpoint, v, authSuffix(meta)), nil) + _, err := client.Do(req) + e = err + } + return +} + +func resourceMonitorExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { + b = true + for _, v := range strings.Split(d.Id(), "__") { + res, err := http.Get(fmt.Sprintf("%s/%s%s", monitorEndpoint, v, authSuffix(meta))) + if err != nil { + e = err + continue + } + if res.StatusCode > 400 { + b = false + continue + } + b = b && true + } + if !b { + e = resourceMonitorDelete(d, meta) + } + return +} + +func resourceMonitorRead(d *schema.ResourceData, meta interface{}) error { + return nil +} + +func resourceMonitorUpdate(d *schema.ResourceData, meta interface{}) error { + split := strings.Split(d.Id(), "__") + warningID, criticalID := split[0], split[1] + + warningBody, _ := marshalMetric(d, "warning") + criticalBody, _ := marshalMetric(d, "critical") + + client := http.Client{} + + reqW, _ := http.NewRequest("PUT", fmt.Sprintf("%s/%s%s", monitorEndpoint, warningID, authSuffix(meta)), bytes.NewReader(warningBody)) + resW, err := client.Do(reqW) + if err != nil { + return fmt.Errorf("error updating warning: %s", err.Error()) + } + resW.Body.Close() + if resW.StatusCode > 400 { + return fmt.Errorf("error updating warning monitor: %s", resW.Status) + } + + reqC, _ := http.NewRequest("PUT", fmt.Sprintf("%s/%s%s", monitorEndpoint, criticalID, authSuffix(meta)), bytes.NewReader(criticalBody)) + resC, err := client.Do(reqC) + if err != nil { + return fmt.Errorf("error updating critical: %s", err.Error()) + } + resW.Body.Close() + if resW.StatusCode > 400 { + return fmt.Errorf("error updating critical monitor: %s", resC.Status) + } + return nil +} From f407eea3f72dfcc8de2d8b63bcbc257ed53b211d Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Fri, 23 Oct 2015 14:42:19 +1100 Subject: [PATCH 2/6] provider/datadog: Various enhancements - Don't drop wildcard if it's the only one. - Remove monitor resource, it's been replaced by metric_alert, outlier_alert and service_check - Refactor to be closer to the API; each resource creates exactly *one* resource, not 2, this removes much unneeded complexity. A warning threshold is now supported by the API. - Remove fuzzy resources like graph, and resources that used them for dashboard and screenboards. I'd welcome these resources, but the current state of Terraform and the Datadog API does not allow these to be implemented in a clean way. - Support multiple thresholds for metric alerts, remove notify argument. --- builtin/providers/datadog/config.go | 23 ++ builtin/providers/datadog/provider.go | 23 +- builtin/providers/datadog/provider_test.go | 38 +++ .../datadog/resource_datadog_metric_alert.go | 180 ++++++++++++++ .../resource_datadog_metric_alert_test.go | 111 +++++++++ .../datadog/resource_datadog_monitor.go | 235 ------------------ .../datadog/resource_datadog_outlier_alert.go | 184 ++++++++++++++ .../resource_datadog_outlier_alert_test.go | 97 ++++++++ .../datadog/resource_datadog_service_check.go | 163 ++++++++++++ .../resource_datadog_service_check_test.go | 96 +++++++ builtin/providers/datadog/resource_helpers.go | 138 ++++++++++ builtin/providers/datadog/test_helpers.go | 34 +++ 12 files changed, 1082 insertions(+), 240 deletions(-) create mode 100644 builtin/providers/datadog/config.go create mode 100644 builtin/providers/datadog/provider_test.go create mode 100644 builtin/providers/datadog/resource_datadog_metric_alert.go create mode 100644 builtin/providers/datadog/resource_datadog_metric_alert_test.go delete mode 100644 builtin/providers/datadog/resource_datadog_monitor.go create mode 100644 builtin/providers/datadog/resource_datadog_outlier_alert.go create mode 100644 builtin/providers/datadog/resource_datadog_outlier_alert_test.go create mode 100644 builtin/providers/datadog/resource_datadog_service_check.go create mode 100644 builtin/providers/datadog/resource_datadog_service_check_test.go create mode 100644 builtin/providers/datadog/resource_helpers.go create mode 100644 builtin/providers/datadog/test_helpers.go diff --git a/builtin/providers/datadog/config.go b/builtin/providers/datadog/config.go new file mode 100644 index 000000000..61f1a60e9 --- /dev/null +++ b/builtin/providers/datadog/config.go @@ -0,0 +1,23 @@ +package datadog + +import ( + "log" + + "github.com/zorkian/go-datadog-api" +) + +// Config holds API and APP keys to authenticate to Datadog. +type Config struct { + APIKey string + APPKey string +} + +// Client returns a new Datadog client. +func (c *Config) Client() (*datadog.Client, error) { + + client := datadog.NewClient(c.APIKey, c.APPKey) + + log.Printf("[INFO] Datadog Client configured ") + + return client, nil +} diff --git a/builtin/providers/datadog/provider.go b/builtin/providers/datadog/provider.go index d66729d7f..069d78e88 100644 --- a/builtin/providers/datadog/provider.go +++ b/builtin/providers/datadog/provider.go @@ -1,10 +1,13 @@ package datadog import ( + "log" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) +// Provider returns a terraform.ResourceProvider. func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ @@ -19,15 +22,25 @@ func Provider() terraform.ResourceProvider { DefaultFunc: schema.EnvDefaultFunc("DATADOG_APP_KEY", nil), }, }, + ResourcesMap: map[string]*schema.Resource{ - "datadog_monitor_metric": datadogMonitorResource(), + "datadog_service_check": resourceDatadogServiceCheck(), + "datadog_metric_alert": resourceDatadogMetricAlert(), + "datadog_outlier_alert": resourceDatadogOutlierAlert(), }, + ConfigureFunc: providerConfigure, } } -func providerConfigure(rd *schema.ResourceData) (interface{}, error) { - apiKey := rd.Get("api_key").(string) - appKey := rd.Get("app_key").(string) - return map[string]string{"api_key": apiKey, "app_key": appKey}, nil +// ProviderConfigure returns a configured client. +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + + config := Config{ + APIKey: d.Get("api_key").(string), + APPKey: d.Get("app_key").(string), + } + + log.Println("[INFO] Initializing Datadog client") + return config.Client() } diff --git a/builtin/providers/datadog/provider_test.go b/builtin/providers/datadog/provider_test.go new file mode 100644 index 000000000..b6c56102e --- /dev/null +++ b/builtin/providers/datadog/provider_test.go @@ -0,0 +1,38 @@ +package datadog + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "datadog": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("DATADOG_API_KEY"); v == "" { + t.Fatal("DATADOG_API_KEY must be set for acceptance tests") + } + if v := os.Getenv("DATADOG_APP_KEY"); v == "" { + t.Fatal("DATADOG_APP_KEY must be set for acceptance tests") + } +} diff --git a/builtin/providers/datadog/resource_datadog_metric_alert.go b/builtin/providers/datadog/resource_datadog_metric_alert.go new file mode 100644 index 000000000..a817d0313 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_metric_alert.go @@ -0,0 +1,180 @@ +package datadog + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/zorkian/go-datadog-api" +) + +// resourceDatadogMetricAlert is a Datadog monitor resource +func resourceDatadogMetricAlert() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogMetricAlertCreate, + Read: resourceDatadogGenericRead, + Update: resourceDatadogMetricAlertUpdate, + Delete: resourceDatadogGenericDelete, + Exists: resourceDatadogGenericExists, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "metric": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "keys": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "time_aggr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "time_window": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "space_aggr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "operator": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "message": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "thresholds": thresholdSchema(), + + // Additional Settings + "notify_no_data": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "no_data_timeframe": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "renotify_interval": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + }, + } +} + +// buildMonitorStruct returns a monitor struct +func buildMetricAlertStruct(d *schema.ResourceData) *datadog.Monitor { + name := d.Get("name").(string) + message := d.Get("message").(string) + timeAggr := d.Get("time_aggr").(string) + timeWindow := d.Get("time_window").(string) + spaceAggr := d.Get("space_aggr").(string) + metric := d.Get("metric").(string) + + // Tags are are no separate resource/gettable, so some trickery is needed + var buffer bytes.Buffer + if raw, ok := d.GetOk("tags"); ok { + list := raw.([]interface{}) + length := (len(list) - 1) + for i, v := range list { + buffer.WriteString(fmt.Sprintf("%s", v)) + if i != length { + buffer.WriteString(",") + } + + } + } + + tagsParsed := buffer.String() + + // Keys are used for multi alerts + var b bytes.Buffer + if raw, ok := d.GetOk("keys"); ok { + list := raw.([]interface{}) + b.WriteString("by {") + length := (len(list) - 1) + for i, v := range list { + b.WriteString(fmt.Sprintf("%s", v)) + if i != length { + b.WriteString(",") + } + + } + b.WriteString("}") + } + + keys := b.String() + + threshold, thresholds := getThresholds(d) + + operator := d.Get("operator").(string) + query := fmt.Sprintf("%s(%s):%s:%s{%s} %s %s %s", timeAggr, + timeWindow, + spaceAggr, + metric, + tagsParsed, + keys, + operator, + threshold) + + log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query)) + + o := datadog.Options{ + NotifyNoData: d.Get("notify_no_data").(bool), + NoDataTimeframe: d.Get("no_data_timeframe").(int), + RenotifyInterval: d.Get("renotify_interval").(int), + Thresholds: thresholds, + } + + m := datadog.Monitor{ + Type: "metric alert", + Query: query, + Name: name, + Message: message, + Options: o, + } + + return &m +} + +// resourceDatadogMetricAlertCreate creates a monitor. +func resourceDatadogMetricAlertCreate(d *schema.ResourceData, meta interface{}) error { + + m := buildMetricAlertStruct(d) + if err := monitorCreator(d, meta, m); err != nil { + return err + } + + return nil +} + +// resourceDatadogMetricAlertUpdate updates a monitor. +func resourceDatadogMetricAlertUpdate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] running update.") + + m := buildMetricAlertStruct(d) + if err := monitorUpdater(d, meta, m); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/datadog/resource_datadog_metric_alert_test.go b/builtin/providers/datadog/resource_datadog_metric_alert_test.go new file mode 100644 index 000000000..6bfcb751c --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_metric_alert_test.go @@ -0,0 +1,111 @@ +package datadog + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/zorkian/go-datadog-api" +) + +func TestAccDatadogMetricAlert_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMetricAlertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogMetricAlertConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMetricAlertExists("datadog_metric_alert.foo"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "name", "name for metric_alert foo"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "message", "{{#is_alert}}Metric alert foo is critical"+ + "{{/is_alert}}\n{{#is_warning}}Metric alert foo is at warning "+ + "level{{/is_warning}}\n{{#is_recovery}}Metric alert foo has "+ + "recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "metric", "aws.ec2.cpu"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "tags.0", "environment:foo"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "tags.1", "host:foo"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "keys.0", "host"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "keys.#", "1"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "time_aggr", "avg"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "time_window", "last_1h"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "space_aggr", "avg"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "operator", ">"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "notify_no_data", "false"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "renotify_interval", "60"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "thresholds.ok", "0"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "thresholds.warning", "1"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "thresholds.critical", "2"), + ), + }, + }, + }) +} + +func testAccCheckDatadogMetricAlertDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := destroyHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogMetricAlertExists(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 + } +} + +const testAccCheckDatadogMetricAlertConfigBasic = ` +resource "datadog_metric_alert" "foo" { + name = "name for metric_alert foo" + message = <, >=, ==, or != + + thresholds { + ok = 0 + warning = 1 + critical = 2 + } + + notify_no_data = false + renotify_interval = 60 +} +` diff --git a/builtin/providers/datadog/resource_datadog_monitor.go b/builtin/providers/datadog/resource_datadog_monitor.go deleted file mode 100644 index 85f4274ff..000000000 --- a/builtin/providers/datadog/resource_datadog_monitor.go +++ /dev/null @@ -1,235 +0,0 @@ -package datadog - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "strconv" - "strings" - - "github.com/hashicorp/terraform/helper/schema" -) - -const ( - monitorEndpoint = "https://app.datadoghq.com/api/v1/monitor" -) - -func datadogMonitorResource() *schema.Resource { - return &schema.Resource{ - Create: resourceMonitorCreate, - Read: resourceMonitorRead, - Update: resourceMonitorUpdate, - Delete: resourceMonitorDelete, - Exists: resourceMonitorExists, - - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - // Metric and Monitor settings - "metric": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "metric_tags": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "*", - }, - "time_aggr": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "time_window": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "space_aggr": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "operator": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "message": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - // Alert Settings - "warning": &schema.Schema{ - Type: schema.TypeMap, - Required: true, - }, - "critical": &schema.Schema{ - Type: schema.TypeMap, - Required: true, - }, - - // Additional Settings - "notify_no_data": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "no_data_timeframe": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - }, - }, - } -} - -func getIDFromResponse(h *http.Response) (string, error) { - body, err := ioutil.ReadAll(h.Body) - if err != nil { - return "", err - } - h.Body.Close() - log.Println(h) - log.Println(string(body)) - v := map[string]interface{}{} - err = json.Unmarshal(body, &v) - if err != nil { - return "", err - } - if id, ok := v["id"]; ok { - return strconv.Itoa(int(id.(float64))), nil - } - return "", fmt.Errorf("error getting ID from response %s", h.Status) -} - -func marshalMetric(d *schema.ResourceData, typeStr string) ([]byte, error) { - name := d.Get("name").(string) - message := d.Get("message").(string) - timeAggr := d.Get("time_aggr").(string) - timeWindow := d.Get("time_window").(string) - spaceAggr := d.Get("space_aggr").(string) - metric := d.Get("metric").(string) - tags := d.Get("metric_tags").(string) - operator := d.Get("operator").(string) - query := fmt.Sprintf("%s(%s):%s:%s{%s} %s %s", timeAggr, timeWindow, spaceAggr, metric, tags, operator, d.Get(fmt.Sprintf("%s.threshold", typeStr))) - - log.Println(query) - m := map[string]interface{}{ - "type": "metric alert", - "query": query, - "name": fmt.Sprintf("[%s] %s", typeStr, name), - "message": fmt.Sprintf("%s %s", message, d.Get(fmt.Sprintf("%s.notify", typeStr))), - "options": map[string]interface{}{ - "notify_no_data": d.Get("notify_no_data").(bool), - "no_data_timeframe": d.Get("no_data_timeframe").(int), - }, - } - return json.Marshal(m) -} - -func authSuffix(meta interface{}) string { - m := meta.(map[string]string) - return fmt.Sprintf("?api_key=%s&application_key=%s", m["api_key"], m["app_key"]) -} - -func resourceMonitorCreate(d *schema.ResourceData, meta interface{}) error { - warningBody, err := marshalMetric(d, "warning") - if err != nil { - return err - } - criticalBody, err := marshalMetric(d, "critical") - if err != nil { - return err - } - - resW, err := http.Post(fmt.Sprintf("%s%s", monitorEndpoint, authSuffix(meta)), "application/json", bytes.NewReader(warningBody)) - if err != nil { - return fmt.Errorf("error creating warning: %s", err.Error()) - } - - resC, err := http.Post(fmt.Sprintf("%s%s", monitorEndpoint, authSuffix(meta)), "application/json", bytes.NewReader(criticalBody)) - if err != nil { - return fmt.Errorf("error creating critical: %s", err.Error()) - } - - warningMonitorID, err := getIDFromResponse(resW) - if err != nil { - return err - } - criticalMonitorID, err := getIDFromResponse(resC) - if err != nil { - return err - } - - d.SetId(fmt.Sprintf("%s__%s", warningMonitorID, criticalMonitorID)) - - return nil -} - -func resourceMonitorDelete(d *schema.ResourceData, meta interface{}) (e error) { - for _, v := range strings.Split(d.Id(), "__") { - client := http.Client{} - req, _ := http.NewRequest("DELETE", fmt.Sprintf("%s/%s%s", monitorEndpoint, v, authSuffix(meta)), nil) - _, err := client.Do(req) - e = err - } - return -} - -func resourceMonitorExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { - b = true - for _, v := range strings.Split(d.Id(), "__") { - res, err := http.Get(fmt.Sprintf("%s/%s%s", monitorEndpoint, v, authSuffix(meta))) - if err != nil { - e = err - continue - } - if res.StatusCode > 400 { - b = false - continue - } - b = b && true - } - if !b { - e = resourceMonitorDelete(d, meta) - } - return -} - -func resourceMonitorRead(d *schema.ResourceData, meta interface{}) error { - return nil -} - -func resourceMonitorUpdate(d *schema.ResourceData, meta interface{}) error { - split := strings.Split(d.Id(), "__") - warningID, criticalID := split[0], split[1] - - warningBody, _ := marshalMetric(d, "warning") - criticalBody, _ := marshalMetric(d, "critical") - - client := http.Client{} - - reqW, _ := http.NewRequest("PUT", fmt.Sprintf("%s/%s%s", monitorEndpoint, warningID, authSuffix(meta)), bytes.NewReader(warningBody)) - resW, err := client.Do(reqW) - if err != nil { - return fmt.Errorf("error updating warning: %s", err.Error()) - } - resW.Body.Close() - if resW.StatusCode > 400 { - return fmt.Errorf("error updating warning monitor: %s", resW.Status) - } - - reqC, _ := http.NewRequest("PUT", fmt.Sprintf("%s/%s%s", monitorEndpoint, criticalID, authSuffix(meta)), bytes.NewReader(criticalBody)) - resC, err := client.Do(reqC) - if err != nil { - return fmt.Errorf("error updating critical: %s", err.Error()) - } - resW.Body.Close() - if resW.StatusCode > 400 { - return fmt.Errorf("error updating critical monitor: %s", resC.Status) - } - return nil -} diff --git a/builtin/providers/datadog/resource_datadog_outlier_alert.go b/builtin/providers/datadog/resource_datadog_outlier_alert.go new file mode 100644 index 000000000..e401c6fd6 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_outlier_alert.go @@ -0,0 +1,184 @@ +package datadog + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/zorkian/go-datadog-api" +) + +// resourceDatadogOutlierAlert is a Datadog monitor resource +func resourceDatadogOutlierAlert() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogOutlierAlertCreate, + Read: resourceDatadogGenericRead, + Update: resourceDatadogOutlierAlertUpdate, + Delete: resourceDatadogGenericDelete, + Exists: resourceDatadogGenericExists, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "metric": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "keys": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "time_aggr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "time_window": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "space_aggr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "message": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "threshold": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + // Additional Settings + "notify_no_data": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "no_data_timeframe": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "algorithm": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "dbscan", + }, + + "renotify_interval": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + }, + } +} + +// buildMonitorStruct returns a monitor struct +func buildOutlierAlertStruct(d *schema.ResourceData) *datadog.Monitor { + name := d.Get("name").(string) + message := d.Get("message").(string) + timeAggr := d.Get("time_aggr").(string) + timeWindow := d.Get("time_window").(string) + spaceAggr := d.Get("space_aggr").(string) + metric := d.Get("metric").(string) + algorithm := d.Get("algorithm").(string) + + // Tags are are no separate resource/gettable, so some trickery is needed + var buffer bytes.Buffer + if raw, ok := d.GetOk("tags"); ok { + list := raw.([]interface{}) + length := (len(list) - 1) + for i, v := range list { + if length > 1 && v == "*" { + log.Print(fmt.Sprintf("[DEBUG] found wildcard, this is not supported for this type: %s", v)) + continue + } + buffer.WriteString(fmt.Sprintf("%s", v)) + if i != length { + buffer.WriteString(",") + } + + } + } + + tagsParsed := buffer.String() + + // Keys are used for multi alerts + var b bytes.Buffer + if raw, ok := d.GetOk("keys"); ok { + list := raw.([]interface{}) + b.WriteString("by {") + length := (len(list) - 1) + for i, v := range list { + b.WriteString(fmt.Sprintf("%s", v)) + if i != length { + b.WriteString(",") + } + + } + b.WriteString("}") + } + + keys := b.String() + + query := fmt.Sprintf("%s(%s):outliers(%s:%s{%s} %s, '%s',%s) > 0", timeAggr, + timeWindow, + spaceAggr, + metric, + tagsParsed, + keys, + algorithm, + d.Get("threshold")) + + log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query)) + + o := datadog.Options{ + NotifyNoData: d.Get("notify_no_data").(bool), + NoDataTimeframe: d.Get("no_data_timeframe").(int), + RenotifyInterval: d.Get("renotify_interval").(int), + } + + m := datadog.Monitor{ + Type: "query alert", + Query: query, + Name: name, + Message: message, + Options: o, + } + + return &m +} + +// resourceDatadogOutlierAlertCreate creates a monitor. +func resourceDatadogOutlierAlertCreate(d *schema.ResourceData, meta interface{}) error { + + m := buildOutlierAlertStruct(d) + if err := monitorCreator(d, meta, m); err != nil { + return err + } + + return nil +} + +// resourceDatadogOutlierAlertUpdate updates a monitor. +func resourceDatadogOutlierAlertUpdate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] running update.") + + m := buildOutlierAlertStruct(d) + if err := monitorUpdater(d, meta, m); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/datadog/resource_datadog_outlier_alert_test.go b/builtin/providers/datadog/resource_datadog_outlier_alert_test.go new file mode 100644 index 000000000..cbe2e26cc --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_outlier_alert_test.go @@ -0,0 +1,97 @@ +package datadog + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/zorkian/go-datadog-api" +) + +func TestAccDatadogOutlierAlert_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogOutlierAlertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogOutlierAlertConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogOutlierAlertExists("datadog_outlier_alert.foo"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "name", "name for outlier_alert foo"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "message", "description for outlier_alert foo @hipchat-name"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "metric", "system.load.5"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "tags.0", "environment:foo"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "tags.1", "host:foo"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "keys.0", "host"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "keys.#", "1"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "time_aggr", "avg"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "time_window", "last_1h"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "space_aggr", "avg"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "notify_no_data", "false"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "algorithm", "mad"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "renotify_interval", "60"), + resource.TestCheckResourceAttr( + "datadog_outlier_alert.foo", "threshold", "2"), + ), + }, + }, + }) +} + +func testAccCheckDatadogOutlierAlertDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := destroyHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogOutlierAlertExists(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 + } +} + +const testAccCheckDatadogOutlierAlertConfigBasic = ` +resource "datadog_outlier_alert" "foo" { + name = "name for outlier_alert foo" + message = "description for outlier_alert foo @hipchat-name" + + algorithm = "mad" + + metric = "system.load.5" + tags = ["environment:foo", "host:foo"] + keys = ["host"] + + time_aggr = "avg" // avg, sum, max, min, change, or pct_change + time_window = "last_1h" // last_#m (5, 10, 15, 30), last_#h (1, 2, 4), or last_1d + space_aggr = "avg" // avg, sum, min, or max + + threshold = 2.0 + + notify_no_data = false + renotify_interval = 60 + +} +` diff --git a/builtin/providers/datadog/resource_datadog_service_check.go b/builtin/providers/datadog/resource_datadog_service_check.go new file mode 100644 index 000000000..dfbbcbf6b --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_service_check.go @@ -0,0 +1,163 @@ +package datadog + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/zorkian/go-datadog-api" +) + +// resourceDatadogServiceCheck is a Datadog monitor resource +func resourceDatadogServiceCheck() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogServiceCheckCreate, + Read: resourceDatadogGenericRead, + Update: resourceDatadogServiceCheckUpdate, + Delete: resourceDatadogGenericDelete, + Exists: resourceDatadogGenericExists, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "check": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "thresholds": thresholdSchema(), + + "tags": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "keys": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "message": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + // Additional Settings + "notify_no_data": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "no_data_timeframe": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "renotify_interval": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + }, + } +} + +// buildServiceCheckStruct returns a monitor struct +func buildServiceCheckStruct(d *schema.ResourceData) *datadog.Monitor { + log.Print("[DEBUG] building monitor struct") + name := d.Get("name").(string) + message := d.Get("message").(string) + + // Tags are are no separate resource/gettable, so some trickery is needed + var buffer bytes.Buffer + if raw, ok := d.GetOk("tags"); ok { + list := raw.([]interface{}) + length := (len(list) - 1) + for i, v := range list { + buffer.WriteString(fmt.Sprintf("\"%s\"", v)) + if i != length { + buffer.WriteString(",") + } + + } + } + + tagsParsed := buffer.String() + + // Keys are used for multi alerts + var b bytes.Buffer + if raw, ok := d.GetOk("keys"); ok { + list := raw.([]interface{}) + b.WriteString(".by(") + length := (len(list) - 1) + for i, v := range list { + b.WriteString(fmt.Sprintf("\"%s\"", v)) + if i != length { + b.WriteString(",") + } + + } + b.WriteString(")") + } + + keys := b.String() + + var monitorName string + var query string + + check := d.Get("check").(string) + + // Examples queries + // "http.can_connect".over("instance:buildeng_http","production").last(2).count_by_status() + // "http.can_connect".over("*").by("host","instance","url").last(2).count_by_status() + + checkCount, thresholds := getThresholds(d) + + query = fmt.Sprintf("\"%s\".over(%s)%s.last(%s).count_by_status()", check, tagsParsed, keys, checkCount) + log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query)) + monitorName = name + + o := datadog.Options{ + NotifyNoData: d.Get("notify_no_data").(bool), + NoDataTimeframe: d.Get("no_data_timeframe").(int), + RenotifyInterval: d.Get("renotify_interval").(int), + Thresholds: thresholds, + } + + m := datadog.Monitor{ + Type: "service check", + Query: query, + Name: monitorName, + Message: message, + Options: o, + } + + return &m +} + +// resourceDatadogServiceCheckCreate creates a monitor. +func resourceDatadogServiceCheckCreate(d *schema.ResourceData, meta interface{}) error { + log.Print("[DEBUG] creating monitor") + + m := buildServiceCheckStruct(d) + if err := monitorCreator(d, meta, m); err != nil { + return err + } + + return nil +} + +// resourceDatadogServiceCheckUpdate updates a monitor. +func resourceDatadogServiceCheckUpdate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] running update.") + + m := buildServiceCheckStruct(d) + if err := monitorUpdater(d, meta, m); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/datadog/resource_datadog_service_check_test.go b/builtin/providers/datadog/resource_datadog_service_check_test.go new file mode 100644 index 000000000..bfa51e757 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_service_check_test.go @@ -0,0 +1,96 @@ +package datadog + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/zorkian/go-datadog-api" +) + +func TestAccDatadogServiceCheck_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogServiceCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogServiceCheckConfigBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogServiceCheckExists("datadog_service_check.bar"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "name", "name for service check bar"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "message", "{{#is_alert}}Service check bar is critical"+ + "{{/is_alert}}\n{{#is_warning}}Service check bar is at warning "+ + "level{{/is_warning}}\n{{#is_recovery}}Service check bar has "+ + "recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "check", "datadog.agent.up"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "notify_no_data", "false"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "tags.0", "environment:foo"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "tags.1", "host:bar"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "keys.0", "foo"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "keys.1", "bar"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "keys.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "thresholds.ok", "0"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "thresholds.warning", "1"), + resource.TestCheckResourceAttr( + "datadog_service_check.bar", "thresholds.critical", "2"), + ), + }, + }, + }) +} + +func testAccCheckDatadogServiceCheckDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := destroyHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogServiceCheckExists(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 + } +} + +const testAccCheckDatadogServiceCheckConfigBasic = ` +resource "datadog_service_check" "bar" { + name = "name for service check bar" + message = < Date: Sat, 23 Jan 2016 17:51:53 +1100 Subject: [PATCH 3/6] provider/datadog: Add `query` parameter to `metric_alert` `query` is used when it is specified by the user, if not `metric`/`tags`/`keys`/`time_aggr`/`window` is used instead. --- .../datadog/resource_datadog_metric_alert.go | 63 ++++++++++++------- .../resource_datadog_metric_alert_test.go | 59 +++++++++++++++++ 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/builtin/providers/datadog/resource_datadog_metric_alert.go b/builtin/providers/datadog/resource_datadog_metric_alert.go index a817d0313..d92591631 100644 --- a/builtin/providers/datadog/resource_datadog_metric_alert.go +++ b/builtin/providers/datadog/resource_datadog_metric_alert.go @@ -24,8 +24,9 @@ func resourceDatadogMetricAlert() *schema.Resource { Required: true, }, "metric": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"query"}, }, "tags": &schema.Schema{ Type: schema.TypeList, @@ -33,21 +34,25 @@ func resourceDatadogMetricAlert() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, "keys": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"query"}, }, "time_aggr": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"query"}, }, "time_window": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"query"}, }, "space_aggr": &schema.Schema{ - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"query"}, }, "operator": &schema.Schema{ Type: schema.TypeString, @@ -58,6 +63,14 @@ func resourceDatadogMetricAlert() *schema.Resource { Required: true, }, + // Optional Query for custom monitors + + "query": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"time_aggr", "time_window", "space_aggr", "metric", "keys"}, + }, + "thresholds": thresholdSchema(), // Additional Settings @@ -89,6 +102,7 @@ func buildMetricAlertStruct(d *schema.ResourceData) *datadog.Monitor { timeWindow := d.Get("time_window").(string) spaceAggr := d.Get("space_aggr").(string) metric := d.Get("metric").(string) + query := d.Get("query").(string) // Tags are are no separate resource/gettable, so some trickery is needed var buffer bytes.Buffer @@ -127,16 +141,23 @@ func buildMetricAlertStruct(d *schema.ResourceData) *datadog.Monitor { threshold, thresholds := getThresholds(d) operator := d.Get("operator").(string) - query := fmt.Sprintf("%s(%s):%s:%s{%s} %s %s %s", timeAggr, - timeWindow, - spaceAggr, - metric, - tagsParsed, - keys, - operator, - threshold) - log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query)) + var q string + + if query == "" { + q = fmt.Sprintf("%s(%s):%s:%s{%s} %s %s %s", timeAggr, + timeWindow, + spaceAggr, + metric, + tagsParsed, + keys, + operator, + threshold) + } else { + q = fmt.Sprintf("%s %s %s", query, operator, threshold) + } + + log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", q)) o := datadog.Options{ NotifyNoData: d.Get("notify_no_data").(bool), @@ -147,7 +168,7 @@ func buildMetricAlertStruct(d *schema.ResourceData) *datadog.Monitor { m := datadog.Monitor{ Type: "metric alert", - Query: query, + Query: q, Name: name, Message: message, Options: o, diff --git a/builtin/providers/datadog/resource_datadog_metric_alert_test.go b/builtin/providers/datadog/resource_datadog_metric_alert_test.go index 6bfcb751c..8bf2d21d8 100644 --- a/builtin/providers/datadog/resource_datadog_metric_alert_test.go +++ b/builtin/providers/datadog/resource_datadog_metric_alert_test.go @@ -61,6 +61,43 @@ func TestAccDatadogMetricAlert_Basic(t *testing.T) { }) } +func TestAccDatadogMetricAlert_Query(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMetricAlertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogMetricAlertConfigQuery, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMetricAlertExists("datadog_metric_alert.foo"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "name", "name for metric_alert foo"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "message", "{{#is_alert}}Metric alert foo is critical"+ + "{{/is_alert}}\n{{#is_warning}}Metric alert foo is at warning "+ + "level{{/is_warning}}\n{{#is_recovery}}Metric alert foo has "+ + "recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host}"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "operator", ">"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "notify_no_data", "false"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "renotify_interval", "60"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "thresholds.ok", "0"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "thresholds.warning", "1"), + resource.TestCheckResourceAttr( + "datadog_metric_alert.foo", "thresholds.critical", "2"), + ), + }, + }, + }) +} + func testAccCheckDatadogMetricAlertDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*datadog.Client) @@ -109,3 +146,25 @@ EOF renotify_interval = 60 } ` +const testAccCheckDatadogMetricAlertConfigQuery = ` +resource "datadog_metric_alert" "foo" { + name = "name for metric_alert foo" + message = <, >=, ==, or != + query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host}" + + thresholds { + ok = 0 + warning = 1 + critical = 2 + } + + notify_no_data = false + renotify_interval = 60 +} +` From eb2407fccf9717f261d65fd224983e2eff8a7c82 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Mon, 1 Feb 2016 22:39:02 +1100 Subject: [PATCH 4/6] provider/datadog: Replace separate monitors with one generic monitor with read functionality. --- builtin/providers/datadog/provider.go | 6 +- .../datadog/resource_datadog_metric_alert.go | 201 ----------- .../resource_datadog_metric_alert_test.go | 170 ---------- .../datadog/resource_datadog_monitor.go | 318 ++++++++++++++++++ .../datadog/resource_datadog_monitor_test.go | 216 ++++++++++++ .../datadog/resource_datadog_outlier_alert.go | 184 ---------- .../resource_datadog_outlier_alert_test.go | 97 ------ .../datadog/resource_datadog_service_check.go | 163 --------- .../resource_datadog_service_check_test.go | 96 ------ builtin/providers/datadog/resource_helpers.go | 138 -------- builtin/providers/datadog/test_helpers.go | 34 -- 11 files changed, 535 insertions(+), 1088 deletions(-) delete mode 100644 builtin/providers/datadog/resource_datadog_metric_alert.go delete mode 100644 builtin/providers/datadog/resource_datadog_metric_alert_test.go create mode 100644 builtin/providers/datadog/resource_datadog_monitor.go create mode 100644 builtin/providers/datadog/resource_datadog_monitor_test.go delete mode 100644 builtin/providers/datadog/resource_datadog_outlier_alert.go delete mode 100644 builtin/providers/datadog/resource_datadog_outlier_alert_test.go delete mode 100644 builtin/providers/datadog/resource_datadog_service_check.go delete mode 100644 builtin/providers/datadog/resource_datadog_service_check_test.go delete mode 100644 builtin/providers/datadog/resource_helpers.go delete mode 100644 builtin/providers/datadog/test_helpers.go diff --git a/builtin/providers/datadog/provider.go b/builtin/providers/datadog/provider.go index 069d78e88..7221f8481 100644 --- a/builtin/providers/datadog/provider.go +++ b/builtin/providers/datadog/provider.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform/terraform" ) -// Provider returns a terraform.ResourceProvider. func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ @@ -24,16 +23,13 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "datadog_service_check": resourceDatadogServiceCheck(), - "datadog_metric_alert": resourceDatadogMetricAlert(), - "datadog_outlier_alert": resourceDatadogOutlierAlert(), + "datadog_monitor": resourceDatadogMonitor(), }, ConfigureFunc: providerConfigure, } } -// ProviderConfigure returns a configured client. func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ diff --git a/builtin/providers/datadog/resource_datadog_metric_alert.go b/builtin/providers/datadog/resource_datadog_metric_alert.go deleted file mode 100644 index d92591631..000000000 --- a/builtin/providers/datadog/resource_datadog_metric_alert.go +++ /dev/null @@ -1,201 +0,0 @@ -package datadog - -import ( - "bytes" - "fmt" - "log" - - "github.com/hashicorp/terraform/helper/schema" - "github.com/zorkian/go-datadog-api" -) - -// resourceDatadogMetricAlert is a Datadog monitor resource -func resourceDatadogMetricAlert() *schema.Resource { - return &schema.Resource{ - Create: resourceDatadogMetricAlertCreate, - Read: resourceDatadogGenericRead, - Update: resourceDatadogMetricAlertUpdate, - Delete: resourceDatadogGenericDelete, - Exists: resourceDatadogGenericExists, - - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "metric": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"query"}, - }, - "tags": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "keys": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - ConflictsWith: []string{"query"}, - }, - "time_aggr": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"query"}, - }, - "time_window": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"query"}, - }, - "space_aggr": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"query"}, - }, - "operator": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "message": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - // Optional Query for custom monitors - - "query": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"time_aggr", "time_window", "space_aggr", "metric", "keys"}, - }, - - "thresholds": thresholdSchema(), - - // Additional Settings - "notify_no_data": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "no_data_timeframe": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - }, - - "renotify_interval": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 0, - }, - }, - } -} - -// buildMonitorStruct returns a monitor struct -func buildMetricAlertStruct(d *schema.ResourceData) *datadog.Monitor { - name := d.Get("name").(string) - message := d.Get("message").(string) - timeAggr := d.Get("time_aggr").(string) - timeWindow := d.Get("time_window").(string) - spaceAggr := d.Get("space_aggr").(string) - metric := d.Get("metric").(string) - query := d.Get("query").(string) - - // Tags are are no separate resource/gettable, so some trickery is needed - var buffer bytes.Buffer - if raw, ok := d.GetOk("tags"); ok { - list := raw.([]interface{}) - length := (len(list) - 1) - for i, v := range list { - buffer.WriteString(fmt.Sprintf("%s", v)) - if i != length { - buffer.WriteString(",") - } - - } - } - - tagsParsed := buffer.String() - - // Keys are used for multi alerts - var b bytes.Buffer - if raw, ok := d.GetOk("keys"); ok { - list := raw.([]interface{}) - b.WriteString("by {") - length := (len(list) - 1) - for i, v := range list { - b.WriteString(fmt.Sprintf("%s", v)) - if i != length { - b.WriteString(",") - } - - } - b.WriteString("}") - } - - keys := b.String() - - threshold, thresholds := getThresholds(d) - - operator := d.Get("operator").(string) - - var q string - - if query == "" { - q = fmt.Sprintf("%s(%s):%s:%s{%s} %s %s %s", timeAggr, - timeWindow, - spaceAggr, - metric, - tagsParsed, - keys, - operator, - threshold) - } else { - q = fmt.Sprintf("%s %s %s", query, operator, threshold) - } - - log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", q)) - - o := datadog.Options{ - NotifyNoData: d.Get("notify_no_data").(bool), - NoDataTimeframe: d.Get("no_data_timeframe").(int), - RenotifyInterval: d.Get("renotify_interval").(int), - Thresholds: thresholds, - } - - m := datadog.Monitor{ - Type: "metric alert", - Query: q, - Name: name, - Message: message, - Options: o, - } - - return &m -} - -// resourceDatadogMetricAlertCreate creates a monitor. -func resourceDatadogMetricAlertCreate(d *schema.ResourceData, meta interface{}) error { - - m := buildMetricAlertStruct(d) - if err := monitorCreator(d, meta, m); err != nil { - return err - } - - return nil -} - -// resourceDatadogMetricAlertUpdate updates a monitor. -func resourceDatadogMetricAlertUpdate(d *schema.ResourceData, meta interface{}) error { - log.Printf("[DEBUG] running update.") - - m := buildMetricAlertStruct(d) - if err := monitorUpdater(d, meta, m); err != nil { - return err - } - - return nil -} diff --git a/builtin/providers/datadog/resource_datadog_metric_alert_test.go b/builtin/providers/datadog/resource_datadog_metric_alert_test.go deleted file mode 100644 index 8bf2d21d8..000000000 --- a/builtin/providers/datadog/resource_datadog_metric_alert_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package datadog - -import ( - "testing" - - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" - "github.com/zorkian/go-datadog-api" -) - -func TestAccDatadogMetricAlert_Basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDatadogMetricAlertDestroy, - Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccCheckDatadogMetricAlertConfigBasic, - Check: resource.ComposeTestCheckFunc( - testAccCheckDatadogMetricAlertExists("datadog_metric_alert.foo"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "name", "name for metric_alert foo"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "message", "{{#is_alert}}Metric alert foo is critical"+ - "{{/is_alert}}\n{{#is_warning}}Metric alert foo is at warning "+ - "level{{/is_warning}}\n{{#is_recovery}}Metric alert foo has "+ - "recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "metric", "aws.ec2.cpu"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "tags.0", "environment:foo"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "tags.1", "host:foo"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "tags.#", "2"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "keys.0", "host"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "keys.#", "1"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "time_aggr", "avg"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "time_window", "last_1h"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "space_aggr", "avg"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "operator", ">"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "notify_no_data", "false"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "renotify_interval", "60"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "thresholds.ok", "0"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "thresholds.warning", "1"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "thresholds.critical", "2"), - ), - }, - }, - }) -} - -func TestAccDatadogMetricAlert_Query(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDatadogMetricAlertDestroy, - Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccCheckDatadogMetricAlertConfigQuery, - Check: resource.ComposeTestCheckFunc( - testAccCheckDatadogMetricAlertExists("datadog_metric_alert.foo"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "name", "name for metric_alert foo"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "message", "{{#is_alert}}Metric alert foo is critical"+ - "{{/is_alert}}\n{{#is_warning}}Metric alert foo is at warning "+ - "level{{/is_warning}}\n{{#is_recovery}}Metric alert foo has "+ - "recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host}"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "operator", ">"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "notify_no_data", "false"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "renotify_interval", "60"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "thresholds.ok", "0"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "thresholds.warning", "1"), - resource.TestCheckResourceAttr( - "datadog_metric_alert.foo", "thresholds.critical", "2"), - ), - }, - }, - }) -} - -func testAccCheckDatadogMetricAlertDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*datadog.Client) - - if err := destroyHelper(s, client); err != nil { - return err - } - return nil -} - -func testAccCheckDatadogMetricAlertExists(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 - } -} - -const testAccCheckDatadogMetricAlertConfigBasic = ` -resource "datadog_metric_alert" "foo" { - name = "name for metric_alert foo" - message = <, >=, ==, or != - - thresholds { - ok = 0 - warning = 1 - critical = 2 - } - - notify_no_data = false - renotify_interval = 60 -} -` -const testAccCheckDatadogMetricAlertConfigQuery = ` -resource "datadog_metric_alert" "foo" { - name = "name for metric_alert foo" - message = <, >=, ==, or != - query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host}" - - thresholds { - ok = 0 - warning = 1 - critical = 2 - } - - notify_no_data = false - renotify_interval = 60 -} -` diff --git a/builtin/providers/datadog/resource_datadog_monitor.go b/builtin/providers/datadog/resource_datadog_monitor.go new file mode 100644 index 000000000..af2b89f53 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_monitor.go @@ -0,0 +1,318 @@ +package datadog + +import ( + "fmt" + "log" + "strconv" + "strings" + + "encoding/json" + "github.com/hashicorp/terraform/helper/schema" + "github.com/zorkian/go-datadog-api" +) + +func resourceDatadogMonitor() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogMonitorCreate, + Read: resourceDatadogMonitorRead, + Update: resourceDatadogMonitorUpdate, + Delete: resourceDatadogMonitorDelete, + Exists: resourceDatadogMonitorExists, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "message": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "escalation_message": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "query": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + // Options + "thresholds": &schema.Schema{ + Type: schema.TypeMap, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ok": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + }, + "warning": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + }, + "critical": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + }, + }, + }, + "notify_no_data": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "no_data_timeframe": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "renotify_interval": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "notify_audit": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "timeout_h": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + // TODO should actually be map[string]int + "silenced": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Elem: &schema.Schema{ + Type: schema.TypeInt}, + }, + }, + "include_tags": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func buildMonitorStruct(d *schema.ResourceData) *datadog.Monitor { + + var thresholds datadog.ThresholdCount + + if r, ok := d.GetOk("thresholds.ok"); ok { + thresholds.Ok = json.Number(r.(string)) + } + if r, ok := d.GetOk("thresholds.warning"); ok { + thresholds.Warning = json.Number(r.(string)) + } + if r, ok := d.GetOk("thresholds.critical"); ok { + thresholds.Critical = json.Number(r.(string)) + } + + o := datadog.Options{ + Thresholds: thresholds, + } + if attr, ok := d.GetOk("silenced"); ok { + s := make(map[string]int) + // TODO: this is not very defensive, test if we can fail on non int input + for k, v := range attr.(map[string]interface{}) { + s[k], _ = strconv.Atoi(v.(string)) + } + o.Silenced = s + } + if attr, ok := d.GetOk("notify_data"); ok { + o.NotifyNoData = attr.(bool) + } + if attr, ok := d.GetOk("no_data_timeframe"); ok { + o.NoDataTimeframe = attr.(int) + } + if attr, ok := d.GetOk("renotify_interval"); ok { + o.RenotifyInterval = attr.(int) + } + if attr, ok := d.GetOk("notify_audit"); ok { + o.NotifyAudit = attr.(bool) + } + if attr, ok := d.GetOk("timeout_h"); ok { + o.TimeoutH = attr.(int) + } + if attr, ok := d.GetOk("escalation_message"); ok { + o.EscalationMessage = attr.(string) + } + if attr, ok := d.GetOk("escalation_message"); ok { + o.EscalationMessage = attr.(string) + } + if attr, ok := d.GetOk("include_tags"); ok { + o.IncludeTags = attr.(bool) + } + + m := datadog.Monitor{ + Type: d.Get("type").(string), + Query: d.Get("query").(string), + Name: d.Get("name").(string), + Message: d.Get("message").(string), + Options: o, + } + + return &m +} + +func resourceDatadogMonitorExists(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) + + i, err := strconv.Atoi(d.Id()) + if err != nil { + return false, err + } + + if _, err = client.GetMonitor(i); err != nil { + if strings.Contains(err.Error(), "404 Not Found") { + return false, nil + } + return false, err + } + + return true, nil +} + +func resourceDatadogMonitorCreate(d *schema.ResourceData, meta interface{}) error { + + client := meta.(*datadog.Client) + + m := buildMonitorStruct(d) + m, err := client.CreateMonitor(m) + if err != nil { + return fmt.Errorf("error updating montor: %s", err.Error()) + } + + d.SetId(strconv.Itoa(m.Id)) + + return nil +} + +func resourceDatadogMonitorRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + i, err := strconv.Atoi(d.Id()) + if err != nil { + return err + } + + m, err := client.GetMonitor(i) + if err != nil { + return err + } + + log.Printf("[DEBUG] monitor: %v", m) + d.Set("name", m.Name) + d.Set("message", m.Message) + d.Set("query", m.Query) + d.Set("type", m.Type) + d.Set("thresholds", m.Options.Thresholds) + d.Set("notify_no_data", m.Options.NotifyNoData) + d.Set("notify_no_data_timeframe", m.Options.NoDataTimeframe) + d.Set("renotify_interval", m.Options.RenotifyInterval) + d.Set("notify_audit", m.Options.NotifyAudit) + d.Set("timeout_h", m.Options.TimeoutH) + d.Set("escalation_message", m.Options.EscalationMessage) + d.Set("silenced", m.Options.Silenced) + d.Set("include_tags", m.Options.IncludeTags) + + return nil +} + +func resourceDatadogMonitorUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + m := &datadog.Monitor{} + + i, err := strconv.Atoi(d.Id()) + if err != nil { + return err + } + + m.Id = i + if attr, ok := d.GetOk("name"); ok { + m.Name = attr.(string) + } + if attr, ok := d.GetOk("message"); ok { + m.Message = attr.(string) + } + if attr, ok := d.GetOk("query"); ok { + m.Query = attr.(string) + } + + o := datadog.Options{} + if attr, ok := d.GetOk("thresholds"); ok { + thresholds := attr.(map[string]interface{}) + if thresholds["ok"] != nil { + o.Thresholds.Ok = json.Number(thresholds["ok"].(string)) + } + if thresholds["warning"] != nil { + o.Thresholds.Warning = json.Number(thresholds["warning"].(string)) + } + if thresholds["critical"] != nil { + o.Thresholds.Critical = json.Number(thresholds["critical"].(string)) + } + } + + if attr, ok := d.GetOk("notify_no_data"); ok { + o.NotifyNoData = attr.(bool) + } + if attr, ok := d.GetOk("notify_no_data_timeframe"); ok { + o.NoDataTimeframe = attr.(int) + } + if attr, ok := d.GetOk("renotify_interval"); ok { + o.RenotifyInterval = attr.(int) + } + if attr, ok := d.GetOk("notify_audit"); ok { + o.NotifyAudit = attr.(bool) + } + if attr, ok := d.GetOk("timeout_h"); ok { + o.TimeoutH = attr.(int) + } + if attr, ok := d.GetOk("escalation_message"); ok { + o.EscalationMessage = attr.(string) + } + if attr, ok := d.GetOk("silenced"); ok { + // TODO: this is not very defensive, test if we can fail non int input + s := make(map[string]int) + for k, v := range attr.(map[string]interface{}) { + s[k], _ = strconv.Atoi(v.(string)) + } + o.Silenced = s + } + if attr, ok := d.GetOk("include_tags"); ok { + o.IncludeTags = attr.(bool) + } + + m.Options = o + + if err = client.UpdateMonitor(m); err != nil { + return fmt.Errorf("error updating montor: %s", err.Error()) + } + + return resourceDatadogMonitorRead(d, meta) +} + +func resourceDatadogMonitorDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + i, err := strconv.Atoi(d.Id()) + if err != nil { + return err + } + + if err = client.DeleteMonitor(i); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/datadog/resource_datadog_monitor_test.go b/builtin/providers/datadog/resource_datadog_monitor_test.go new file mode 100644 index 000000000..f91019d41 --- /dev/null +++ b/builtin/providers/datadog/resource_datadog_monitor_test.go @@ -0,0 +1,216 @@ +package datadog + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/zorkian/go-datadog-api" +) + +func TestAccDatadogMonitor_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMonitorDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogMonitorConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "message", "some message Notify: @hipchat-channel"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "metric alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "notify_no_data", "false"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "renotify_interval", "60"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.ok", "0"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.warning", "1"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.critical", "2"), + ), + }, + }, + }) +} + +func TestAccDatadogMonitor_Updated(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogMonitorDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDatadogMonitorConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "message", "some message Notify: @hipchat-channel"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "escalation_message", "the situation has escalated @pagerduty"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "metric alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "notify_no_data", "false"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "renotify_interval", "60"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.ok", "0"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.warning", "1"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.critical", "2"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "notify_audit", "false"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "timeout_h", "60"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "include_tags", "true"), + ), + }, + resource.TestStep{ + Config: testAccCheckDatadogMonitorConfigUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogMonitorExists("datadog_monitor.foo"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "name", "name for monitor bar"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "message", "a different message Notify: @hipchat-channel"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "query", "avg(last_1h):avg:aws.ec2.cpu{environment:bar,host:bar} by {host} > 3"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "escalation_message", "the situation has escalated! @pagerduty"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "type", "metric alert"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "notify_no_data", "true"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "renotify_interval", "40"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.ok", "0"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.warning", "1"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "thresholds.critical", "3"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "notify_audit", "true"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "timeout_h", "70"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "include_tags", "false"), + resource.TestCheckResourceAttr( + "datadog_monitor.foo", "silenced.*", "0"), + ), + }, + }, + }) +} + +func testAccCheckDatadogMonitorDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := destroyHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogMonitorExists(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 + } +} + +const testAccCheckDatadogMonitorConfig = ` +resource "datadog_monitor" "foo" { + name = "name for monitor foo" + type = "metric alert" + message = "some message Notify: @hipchat-channel" + escalation_message = "the situation has escalated @pagerduty" + + query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2" + + thresholds { + ok = 0 + warning = 1 + critical = 2 + } + + notify_no_data = false + renotify_interval = 60 + + notify_audit = false + timeout_h = 60 + include_tags = true +} +` + +const testAccCheckDatadogMonitorConfigUpdated = ` +resource "datadog_monitor" "foo" { + name = "name for monitor bar" + type = "metric alert" + message = "a different message Notify: @hipchat-channel" + escalation_message = "the situation has escalated @pagerduty" + + query = "avg(last_1h):avg:aws.ec2.cpu{environment:bar,host:bar} by {host} > 3" + + thresholds { + ok = 0 + warning = 1 + critical = 3 + } + + notify_no_data = true + renotify_interval = 40 + escalation_message = "the situation has escalated! @pagerduty" + notify_audit = true + timeout_h = 70 + include_tags = false + silenced { + "*" = 0 + } +} +` + +func destroyHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + i, _ := strconv.Atoi(r.Primary.ID) + if _, err := client.GetMonitor(i); err != nil { + if strings.Contains(err.Error(), "404 Not Found") { + continue + } + return fmt.Errorf("Received an error retrieving monitor %s", err) + } + return fmt.Errorf("Monitor still exists") + } + return nil +} + +func existsHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + i, _ := strconv.Atoi(r.Primary.ID) + if _, err := client.GetMonitor(i); err != nil { + return fmt.Errorf("Received an error retrieving monitor %s", err) + } + } + return nil +} diff --git a/builtin/providers/datadog/resource_datadog_outlier_alert.go b/builtin/providers/datadog/resource_datadog_outlier_alert.go deleted file mode 100644 index e401c6fd6..000000000 --- a/builtin/providers/datadog/resource_datadog_outlier_alert.go +++ /dev/null @@ -1,184 +0,0 @@ -package datadog - -import ( - "bytes" - "fmt" - "log" - - "github.com/hashicorp/terraform/helper/schema" - "github.com/zorkian/go-datadog-api" -) - -// resourceDatadogOutlierAlert is a Datadog monitor resource -func resourceDatadogOutlierAlert() *schema.Resource { - return &schema.Resource{ - Create: resourceDatadogOutlierAlertCreate, - Read: resourceDatadogGenericRead, - Update: resourceDatadogOutlierAlertUpdate, - Delete: resourceDatadogGenericDelete, - Exists: resourceDatadogGenericExists, - - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "metric": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "tags": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "keys": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "time_aggr": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "time_window": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "space_aggr": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "message": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "threshold": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - // Additional Settings - "notify_no_data": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "no_data_timeframe": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - }, - - "algorithm": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "dbscan", - }, - - "renotify_interval": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 0, - }, - }, - } -} - -// buildMonitorStruct returns a monitor struct -func buildOutlierAlertStruct(d *schema.ResourceData) *datadog.Monitor { - name := d.Get("name").(string) - message := d.Get("message").(string) - timeAggr := d.Get("time_aggr").(string) - timeWindow := d.Get("time_window").(string) - spaceAggr := d.Get("space_aggr").(string) - metric := d.Get("metric").(string) - algorithm := d.Get("algorithm").(string) - - // Tags are are no separate resource/gettable, so some trickery is needed - var buffer bytes.Buffer - if raw, ok := d.GetOk("tags"); ok { - list := raw.([]interface{}) - length := (len(list) - 1) - for i, v := range list { - if length > 1 && v == "*" { - log.Print(fmt.Sprintf("[DEBUG] found wildcard, this is not supported for this type: %s", v)) - continue - } - buffer.WriteString(fmt.Sprintf("%s", v)) - if i != length { - buffer.WriteString(",") - } - - } - } - - tagsParsed := buffer.String() - - // Keys are used for multi alerts - var b bytes.Buffer - if raw, ok := d.GetOk("keys"); ok { - list := raw.([]interface{}) - b.WriteString("by {") - length := (len(list) - 1) - for i, v := range list { - b.WriteString(fmt.Sprintf("%s", v)) - if i != length { - b.WriteString(",") - } - - } - b.WriteString("}") - } - - keys := b.String() - - query := fmt.Sprintf("%s(%s):outliers(%s:%s{%s} %s, '%s',%s) > 0", timeAggr, - timeWindow, - spaceAggr, - metric, - tagsParsed, - keys, - algorithm, - d.Get("threshold")) - - log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query)) - - o := datadog.Options{ - NotifyNoData: d.Get("notify_no_data").(bool), - NoDataTimeframe: d.Get("no_data_timeframe").(int), - RenotifyInterval: d.Get("renotify_interval").(int), - } - - m := datadog.Monitor{ - Type: "query alert", - Query: query, - Name: name, - Message: message, - Options: o, - } - - return &m -} - -// resourceDatadogOutlierAlertCreate creates a monitor. -func resourceDatadogOutlierAlertCreate(d *schema.ResourceData, meta interface{}) error { - - m := buildOutlierAlertStruct(d) - if err := monitorCreator(d, meta, m); err != nil { - return err - } - - return nil -} - -// resourceDatadogOutlierAlertUpdate updates a monitor. -func resourceDatadogOutlierAlertUpdate(d *schema.ResourceData, meta interface{}) error { - log.Printf("[DEBUG] running update.") - - m := buildOutlierAlertStruct(d) - if err := monitorUpdater(d, meta, m); err != nil { - return err - } - - return nil -} diff --git a/builtin/providers/datadog/resource_datadog_outlier_alert_test.go b/builtin/providers/datadog/resource_datadog_outlier_alert_test.go deleted file mode 100644 index cbe2e26cc..000000000 --- a/builtin/providers/datadog/resource_datadog_outlier_alert_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package datadog - -import ( - "testing" - - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" - "github.com/zorkian/go-datadog-api" -) - -func TestAccDatadogOutlierAlert_Basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDatadogOutlierAlertDestroy, - Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccCheckDatadogOutlierAlertConfigBasic, - Check: resource.ComposeTestCheckFunc( - testAccCheckDatadogOutlierAlertExists("datadog_outlier_alert.foo"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "name", "name for outlier_alert foo"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "message", "description for outlier_alert foo @hipchat-name"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "metric", "system.load.5"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "tags.0", "environment:foo"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "tags.1", "host:foo"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "tags.#", "2"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "keys.0", "host"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "keys.#", "1"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "time_aggr", "avg"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "time_window", "last_1h"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "space_aggr", "avg"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "notify_no_data", "false"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "algorithm", "mad"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "renotify_interval", "60"), - resource.TestCheckResourceAttr( - "datadog_outlier_alert.foo", "threshold", "2"), - ), - }, - }, - }) -} - -func testAccCheckDatadogOutlierAlertDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*datadog.Client) - - if err := destroyHelper(s, client); err != nil { - return err - } - return nil -} - -func testAccCheckDatadogOutlierAlertExists(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 - } -} - -const testAccCheckDatadogOutlierAlertConfigBasic = ` -resource "datadog_outlier_alert" "foo" { - name = "name for outlier_alert foo" - message = "description for outlier_alert foo @hipchat-name" - - algorithm = "mad" - - metric = "system.load.5" - tags = ["environment:foo", "host:foo"] - keys = ["host"] - - time_aggr = "avg" // avg, sum, max, min, change, or pct_change - time_window = "last_1h" // last_#m (5, 10, 15, 30), last_#h (1, 2, 4), or last_1d - space_aggr = "avg" // avg, sum, min, or max - - threshold = 2.0 - - notify_no_data = false - renotify_interval = 60 - -} -` diff --git a/builtin/providers/datadog/resource_datadog_service_check.go b/builtin/providers/datadog/resource_datadog_service_check.go deleted file mode 100644 index dfbbcbf6b..000000000 --- a/builtin/providers/datadog/resource_datadog_service_check.go +++ /dev/null @@ -1,163 +0,0 @@ -package datadog - -import ( - "bytes" - "fmt" - "log" - - "github.com/hashicorp/terraform/helper/schema" - "github.com/zorkian/go-datadog-api" -) - -// resourceDatadogServiceCheck is a Datadog monitor resource -func resourceDatadogServiceCheck() *schema.Resource { - return &schema.Resource{ - Create: resourceDatadogServiceCheckCreate, - Read: resourceDatadogGenericRead, - Update: resourceDatadogServiceCheckUpdate, - Delete: resourceDatadogGenericDelete, - Exists: resourceDatadogGenericExists, - - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "check": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - - "thresholds": thresholdSchema(), - - "tags": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "keys": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "message": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - - // Additional Settings - "notify_no_data": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - - "no_data_timeframe": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - }, - "renotify_interval": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 0, - }, - }, - } -} - -// buildServiceCheckStruct returns a monitor struct -func buildServiceCheckStruct(d *schema.ResourceData) *datadog.Monitor { - log.Print("[DEBUG] building monitor struct") - name := d.Get("name").(string) - message := d.Get("message").(string) - - // Tags are are no separate resource/gettable, so some trickery is needed - var buffer bytes.Buffer - if raw, ok := d.GetOk("tags"); ok { - list := raw.([]interface{}) - length := (len(list) - 1) - for i, v := range list { - buffer.WriteString(fmt.Sprintf("\"%s\"", v)) - if i != length { - buffer.WriteString(",") - } - - } - } - - tagsParsed := buffer.String() - - // Keys are used for multi alerts - var b bytes.Buffer - if raw, ok := d.GetOk("keys"); ok { - list := raw.([]interface{}) - b.WriteString(".by(") - length := (len(list) - 1) - for i, v := range list { - b.WriteString(fmt.Sprintf("\"%s\"", v)) - if i != length { - b.WriteString(",") - } - - } - b.WriteString(")") - } - - keys := b.String() - - var monitorName string - var query string - - check := d.Get("check").(string) - - // Examples queries - // "http.can_connect".over("instance:buildeng_http","production").last(2).count_by_status() - // "http.can_connect".over("*").by("host","instance","url").last(2).count_by_status() - - checkCount, thresholds := getThresholds(d) - - query = fmt.Sprintf("\"%s\".over(%s)%s.last(%s).count_by_status()", check, tagsParsed, keys, checkCount) - log.Print(fmt.Sprintf("[DEBUG] submitting query: %s", query)) - monitorName = name - - o := datadog.Options{ - NotifyNoData: d.Get("notify_no_data").(bool), - NoDataTimeframe: d.Get("no_data_timeframe").(int), - RenotifyInterval: d.Get("renotify_interval").(int), - Thresholds: thresholds, - } - - m := datadog.Monitor{ - Type: "service check", - Query: query, - Name: monitorName, - Message: message, - Options: o, - } - - return &m -} - -// resourceDatadogServiceCheckCreate creates a monitor. -func resourceDatadogServiceCheckCreate(d *schema.ResourceData, meta interface{}) error { - log.Print("[DEBUG] creating monitor") - - m := buildServiceCheckStruct(d) - if err := monitorCreator(d, meta, m); err != nil { - return err - } - - return nil -} - -// resourceDatadogServiceCheckUpdate updates a monitor. -func resourceDatadogServiceCheckUpdate(d *schema.ResourceData, meta interface{}) error { - log.Printf("[DEBUG] running update.") - - m := buildServiceCheckStruct(d) - if err := monitorUpdater(d, meta, m); err != nil { - return err - } - - return nil -} diff --git a/builtin/providers/datadog/resource_datadog_service_check_test.go b/builtin/providers/datadog/resource_datadog_service_check_test.go deleted file mode 100644 index bfa51e757..000000000 --- a/builtin/providers/datadog/resource_datadog_service_check_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package datadog - -import ( - "testing" - - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/terraform" - "github.com/zorkian/go-datadog-api" -) - -func TestAccDatadogServiceCheck_Basic(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckDatadogServiceCheckDestroy, - Steps: []resource.TestStep{ - resource.TestStep{ - Config: testAccCheckDatadogServiceCheckConfigBasic, - Check: resource.ComposeTestCheckFunc( - testAccCheckDatadogServiceCheckExists("datadog_service_check.bar"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "name", "name for service check bar"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "message", "{{#is_alert}}Service check bar is critical"+ - "{{/is_alert}}\n{{#is_warning}}Service check bar is at warning "+ - "level{{/is_warning}}\n{{#is_recovery}}Service check bar has "+ - "recovered{{/is_recovery}}\nNotify: @hipchat-channel\n"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "check", "datadog.agent.up"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "notify_no_data", "false"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "tags.0", "environment:foo"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "tags.1", "host:bar"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "tags.#", "2"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "keys.0", "foo"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "keys.1", "bar"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "keys.#", "2"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "thresholds.ok", "0"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "thresholds.warning", "1"), - resource.TestCheckResourceAttr( - "datadog_service_check.bar", "thresholds.critical", "2"), - ), - }, - }, - }) -} - -func testAccCheckDatadogServiceCheckDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*datadog.Client) - - if err := destroyHelper(s, client); err != nil { - return err - } - return nil -} - -func testAccCheckDatadogServiceCheckExists(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 - } -} - -const testAccCheckDatadogServiceCheckConfigBasic = ` -resource "datadog_service_check" "bar" { - name = "name for service check bar" - message = < Date: Mon, 8 Feb 2016 00:16:07 +0200 Subject: [PATCH 5/6] provider/datadog: Vendor go-datadog-api --- Godeps/Godeps.json | 8 + vendor/github.com/cenkalti/backoff/.gitignore | 22 + .../github.com/cenkalti/backoff/.travis.yml | 2 + vendor/github.com/cenkalti/backoff/LICENSE | 20 + vendor/github.com/cenkalti/backoff/README.md | 116 +++ .../cenkalti/backoff/adv_example_test.go | 117 +++ vendor/github.com/cenkalti/backoff/backoff.go | 59 ++ .../cenkalti/backoff/backoff_test.go | 27 + .../cenkalti/backoff/example_test.go | 51 ++ .../cenkalti/backoff/exponential.go | 151 ++++ .../cenkalti/backoff/exponential_test.go | 108 +++ vendor/github.com/cenkalti/backoff/retry.go | 46 ++ .../github.com/cenkalti/backoff/retry_test.go | 34 + vendor/github.com/cenkalti/backoff/ticker.go | 79 ++ .../cenkalti/backoff/ticker_test.go | 45 + .../github.com/zorkian/go-datadog-api/LICENSE | 30 + .../zorkian/go-datadog-api/Makefile | 42 + .../zorkian/go-datadog-api/README.md | 69 ++ .../zorkian/go-datadog-api/alerts.go | 85 ++ .../zorkian/go-datadog-api/comments.go | 65 ++ .../zorkian/go-datadog-api/dashboards.go | 107 +++ .../zorkian/go-datadog-api/downtimes.go | 83 ++ .../zorkian/go-datadog-api/events.go | 89 ++ .../integration/dashboards_test.go | 138 ++++ .../integration/downtime_test.go | 110 +++ .../integration/monitors_test.go | 152 ++++ .../integration/screen_widgets_test.go | 766 ++++++++++++++++++ .../integration/screenboards_test.go | 143 ++++ .../integration/test_helpers.go | 24 + .../github.com/zorkian/go-datadog-api/main.go | 30 + .../zorkian/go-datadog-api/monitors.go | 113 +++ .../zorkian/go-datadog-api/request.go | 131 +++ .../zorkian/go-datadog-api/screen_widgets.go | 287 +++++++ .../zorkian/go-datadog-api/screenboards.go | 117 +++ .../zorkian/go-datadog-api/search.go | 37 + .../zorkian/go-datadog-api/series.go | 68 ++ .../zorkian/go-datadog-api/snapshot.go | 33 + .../github.com/zorkian/go-datadog-api/tags.go | 96 +++ .../zorkian/go-datadog-api/users.go | 71 ++ 39 files changed, 3771 insertions(+) create mode 100644 vendor/github.com/cenkalti/backoff/.gitignore create mode 100644 vendor/github.com/cenkalti/backoff/.travis.yml create mode 100644 vendor/github.com/cenkalti/backoff/LICENSE create mode 100644 vendor/github.com/cenkalti/backoff/README.md create mode 100644 vendor/github.com/cenkalti/backoff/adv_example_test.go create mode 100644 vendor/github.com/cenkalti/backoff/backoff.go create mode 100644 vendor/github.com/cenkalti/backoff/backoff_test.go create mode 100644 vendor/github.com/cenkalti/backoff/example_test.go create mode 100644 vendor/github.com/cenkalti/backoff/exponential.go create mode 100644 vendor/github.com/cenkalti/backoff/exponential_test.go create mode 100644 vendor/github.com/cenkalti/backoff/retry.go create mode 100644 vendor/github.com/cenkalti/backoff/retry_test.go create mode 100644 vendor/github.com/cenkalti/backoff/ticker.go create mode 100644 vendor/github.com/cenkalti/backoff/ticker_test.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/LICENSE create mode 100644 vendor/github.com/zorkian/go-datadog-api/Makefile create mode 100644 vendor/github.com/zorkian/go-datadog-api/README.md create mode 100644 vendor/github.com/zorkian/go-datadog-api/alerts.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/comments.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/dashboards.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/downtimes.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/events.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/integration/dashboards_test.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/integration/downtime_test.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/integration/monitors_test.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/integration/screen_widgets_test.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/integration/screenboards_test.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/integration/test_helpers.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/main.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/monitors.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/request.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/screen_widgets.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/screenboards.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/search.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/series.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/snapshot.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/tags.go create mode 100644 vendor/github.com/zorkian/go-datadog-api/users.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 2c7cb8750..5b2aa1bfb 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -396,6 +396,10 @@ "ImportPath": "github.com/bgentry/speakeasy", "Rev": "36e9cfdd690967f4f690c6edcc9ffacd006014a0" }, + { + "ImportPath": "github.com/cenkalti/backoff", + "Rev": "4dc77674aceaabba2c7e3da25d4c823edfb73f99" + }, { "ImportPath": "github.com/coreos/etcd/Godeps/_workspace/src/github.com/ugorji/go/codec", "Comment": "v2.3.0-alpha.0-652-ge552791", @@ -717,6 +721,10 @@ "Comment": "v1.5.4-13-g75ce5fb", "Rev": "75ce5fbba34b1912a3641adbd58cf317d7315821" }, + { + "ImportPath": "github.com/zorkian/go-datadog-api", + "Rev": "632146c79714fe4232b496087802f922c1daf96f" + }, { "ImportPath": "golang.org/x/crypto/curve25519", "Rev": "1f22c0103821b9390939b6776727195525381532" diff --git a/vendor/github.com/cenkalti/backoff/.gitignore b/vendor/github.com/cenkalti/backoff/.gitignore new file mode 100644 index 000000000..00268614f --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/cenkalti/backoff/.travis.yml b/vendor/github.com/cenkalti/backoff/.travis.yml new file mode 100644 index 000000000..ce9cb6233 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/.travis.yml @@ -0,0 +1,2 @@ +language: go +go: 1.3.3 diff --git a/vendor/github.com/cenkalti/backoff/LICENSE b/vendor/github.com/cenkalti/backoff/LICENSE new file mode 100644 index 000000000..89b817996 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Cenk Altı + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/cenkalti/backoff/README.md b/vendor/github.com/cenkalti/backoff/README.md new file mode 100644 index 000000000..020b8fbf3 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/README.md @@ -0,0 +1,116 @@ +# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] + +This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. + +[Exponential backoff][exponential backoff wiki] +is an algorithm that uses feedback to multiplicatively decrease the rate of some process, +in order to gradually find an acceptable rate. +The retries exponentially increase and stop increasing when a certain threshold is met. + +## How To + +We define two functions, `Retry()` and `RetryNotify()`. +They receive an `Operation` to execute, a `BackOff` algorithm, +and an optional `Notify` error handler. + +The operation will be executed, and will be retried on failure with delay +as given by the backoff algorithm. The backoff algorithm can also decide when to stop +retrying. +In addition, the notify error handler will be called after each failed attempt, +except for the last time, whose error should be handled by the caller. + +```go +// An Operation is executing by Retry() or RetryNotify(). +// The operation will be retried using a backoff policy if it returns an error. +type Operation func() error + +// Notify is a notify-on-error function. It receives an operation error and +// backoff delay if the operation failed (with an error). +// +// NOTE that if the backoff policy stated to stop retrying, +// the notify function isn't called. +type Notify func(error, time.Duration) + +func Retry(Operation, BackOff) error +func RetryNotify(Operation, BackOff, Notify) +``` + +## Examples + +See more advanced examples in the [godoc][advanced example]. + +### Retry + +Simple retry helper that uses the default exponential backoff algorithm: + +```go +operation := func() error { + // An operation that might fail. + return nil // or return errors.New("some error") +} + +err := Retry(operation, NewExponentialBackOff()) +if err != nil { + // Handle error. + return err +} + +// Operation is successful. +return nil +``` + +### Ticker + +```go +operation := func() error { + // An operation that might fail + return nil // or return errors.New("some error") +} + +b := NewExponentialBackOff() +ticker := NewTicker(b) + +var err error + +// Ticks will continue to arrive when the previous operation is still running, +// so operations that take a while to fail could run in quick succession. +for range ticker.C { + if err = operation(); err != nil { + log.Println(err, "will retry...") + continue + } + + ticker.Stop() + break +} + +if err != nil { + // Operation has failed. + return err +} + +// Operation is successful. +return nil +``` + +## Getting Started + +```bash +# install +$ go get github.com/cenkalti/backoff + +# test +$ cd $GOPATH/src/github.com/cenkalti/backoff +$ go get -t ./... +$ go test -v -cover +``` + +[godoc]: https://godoc.org/github.com/cenkalti/backoff +[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png +[travis]: https://travis-ci.org/cenkalti/backoff +[travis image]: https://travis-ci.org/cenkalti/backoff.png + +[google-http-java-client]: https://github.com/google/google-http-java-client +[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff + +[advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_ diff --git a/vendor/github.com/cenkalti/backoff/adv_example_test.go b/vendor/github.com/cenkalti/backoff/adv_example_test.go new file mode 100644 index 000000000..3fe6783b8 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/adv_example_test.go @@ -0,0 +1,117 @@ +package backoff + +import ( + "io/ioutil" + "log" + "net/http" + "time" +) + +// This is an example that demonstrates how this package could be used +// to perform various advanced operations. +// +// It executes an HTTP GET request with exponential backoff, +// while errors are logged and failed responses are closed, as required by net/http package. +// +// Note we define a condition function which is used inside the operation to +// determine whether the operation succeeded or failed. +func Example() error { + res, err := GetWithRetry( + "http://localhost:9999", + ErrorIfStatusCodeIsNot(http.StatusOK), + NewExponentialBackOff()) + + if err != nil { + // Close response body of last (failed) attempt. + // The Last attempt isn't handled by the notify-on-error function, + // which closes the body of all the previous attempts. + if e := res.Body.Close(); e != nil { + log.Printf("error closing last attempt's response body: %s", e) + } + log.Printf("too many failed request attempts: %s", err) + return err + } + defer res.Body.Close() // The response's Body must be closed. + + // Read body + _, _ = ioutil.ReadAll(res.Body) + + // Do more stuff + return nil +} + +// GetWithRetry is a helper function that performs an HTTP GET request +// to the given URL, and retries with the given backoff using the given condition function. +// +// It also uses a notify-on-error function which logs +// and closes the response body of the failed request. +func GetWithRetry(url string, condition Condition, bck BackOff) (*http.Response, error) { + var res *http.Response + err := RetryNotify( + func() error { + var err error + res, err = http.Get(url) + if err != nil { + return err + } + return condition(res) + }, + bck, + LogAndClose()) + + return res, err +} + +// Condition is a retry condition function. +// It receives a response, and returns an error +// if the response failed the condition. +type Condition func(*http.Response) error + +// ErrorIfStatusCodeIsNot returns a retry condition function. +// The condition returns an error +// if the given response's status code is not the given HTTP status code. +func ErrorIfStatusCodeIsNot(status int) Condition { + return func(res *http.Response) error { + if res.StatusCode != status { + return NewError(res) + } + return nil + } +} + +// Error is returned on ErrorIfX() condition functions throughout this package. +type Error struct { + Response *http.Response +} + +func NewError(res *http.Response) *Error { + // Sanity check + if res == nil { + panic("response object is nil") + } + return &Error{Response: res} +} +func (err *Error) Error() string { return "request failed" } + +// LogAndClose is a notify-on-error function. +// It logs the error and closes the response body. +func LogAndClose() Notify { + return func(err error, wait time.Duration) { + switch e := err.(type) { + case *Error: + defer e.Response.Body.Close() + + b, err := ioutil.ReadAll(e.Response.Body) + var body string + if err != nil { + body = "can't read body" + } else { + body = string(b) + } + + log.Printf("%s: %s", e.Response.Status, body) + default: + log.Println(err) + } + } +} diff --git a/vendor/github.com/cenkalti/backoff/backoff.go b/vendor/github.com/cenkalti/backoff/backoff.go new file mode 100644 index 000000000..61bd6df66 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/backoff.go @@ -0,0 +1,59 @@ +// Package backoff implements backoff algorithms for retrying operations. +// +// Also has a Retry() helper for retrying operations that may fail. +package backoff + +import "time" + +// BackOff is a backoff policy for retrying an operation. +type BackOff interface { + // NextBackOff returns the duration to wait before retrying the operation, + // or backoff.Stop to indicate that no more retries should be made. + // + // Example usage: + // + // duration := backoff.NextBackOff(); + // if (duration == backoff.Stop) { + // // Do not retry operation. + // } else { + // // Sleep for duration and retry operation. + // } + // + NextBackOff() time.Duration + + // Reset to initial state. + Reset() +} + +// Indicates that no more retries should be made for use in NextBackOff(). +const Stop time.Duration = -1 + +// ZeroBackOff is a fixed backoff policy whose backoff time is always zero, +// meaning that the operation is retried immediately without waiting, indefinitely. +type ZeroBackOff struct{} + +func (b *ZeroBackOff) Reset() {} + +func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 } + +// StopBackOff is a fixed backoff policy that always returns backoff.Stop for +// NextBackOff(), meaning that the operation should never be retried. +type StopBackOff struct{} + +func (b *StopBackOff) Reset() {} + +func (b *StopBackOff) NextBackOff() time.Duration { return Stop } + +// ConstantBackOff is a backoff policy that always returns the same backoff delay. +// This is in contrast to an exponential backoff policy, +// which returns a delay that grows longer as you call NextBackOff() over and over again. +type ConstantBackOff struct { + Interval time.Duration +} + +func (b *ConstantBackOff) Reset() {} +func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval } + +func NewConstantBackOff(d time.Duration) *ConstantBackOff { + return &ConstantBackOff{Interval: d} +} diff --git a/vendor/github.com/cenkalti/backoff/backoff_test.go b/vendor/github.com/cenkalti/backoff/backoff_test.go new file mode 100644 index 000000000..91f27c4f1 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/backoff_test.go @@ -0,0 +1,27 @@ +package backoff + +import ( + "testing" + "time" +) + +func TestNextBackOffMillis(t *testing.T) { + subtestNextBackOff(t, 0, new(ZeroBackOff)) + subtestNextBackOff(t, Stop, new(StopBackOff)) +} + +func subtestNextBackOff(t *testing.T, expectedValue time.Duration, backOffPolicy BackOff) { + for i := 0; i < 10; i++ { + next := backOffPolicy.NextBackOff() + if next != expectedValue { + t.Errorf("got: %d expected: %d", next, expectedValue) + } + } +} + +func TestConstantBackOff(t *testing.T) { + backoff := NewConstantBackOff(time.Second) + if backoff.NextBackOff() != time.Second { + t.Error("invalid interval") + } +} diff --git a/vendor/github.com/cenkalti/backoff/example_test.go b/vendor/github.com/cenkalti/backoff/example_test.go new file mode 100644 index 000000000..0d1852e45 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/example_test.go @@ -0,0 +1,51 @@ +package backoff + +import "log" + +func ExampleRetry() error { + operation := func() error { + // An operation that might fail. + return nil // or return errors.New("some error") + } + + err := Retry(operation, NewExponentialBackOff()) + if err != nil { + // Handle error. + return err + } + + // Operation is successful. + return nil +} + +func ExampleTicker() error { + operation := func() error { + // An operation that might fail + return nil // or return errors.New("some error") + } + + b := NewExponentialBackOff() + ticker := NewTicker(b) + + var err error + + // Ticks will continue to arrive when the previous operation is still running, + // so operations that take a while to fail could run in quick succession. + for _ = range ticker.C { + if err = operation(); err != nil { + log.Println(err, "will retry...") + continue + } + + ticker.Stop() + break + } + + if err != nil { + // Operation has failed. + return err + } + + // Operation is successful. + return nil +} diff --git a/vendor/github.com/cenkalti/backoff/exponential.go b/vendor/github.com/cenkalti/backoff/exponential.go new file mode 100644 index 000000000..cc2a164f2 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/exponential.go @@ -0,0 +1,151 @@ +package backoff + +import ( + "math/rand" + "time" +) + +/* +ExponentialBackOff is a backoff implementation that increases the backoff +period for each retry attempt using a randomization function that grows exponentially. + +NextBackOff() is calculated using the following formula: + + randomized interval = + RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + +In other words NextBackOff() will range between the randomization factor +percentage below and above the retry interval. + +For example, given the following parameters: + + RetryInterval = 2 + RandomizationFactor = 0.5 + Multiplier = 2 + +the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, +multiplied by the exponential, that is, between 2 and 6 seconds. + +Note: MaxInterval caps the RetryInterval and not the randomized interval. + +If the time elapsed since an ExponentialBackOff instance is created goes past the +MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. + +The elapsed time can be reset by calling Reset(). + +Example: Given the following default arguments, for 10 tries the sequence will be, +and assuming we go over the MaxElapsedTime on the 10th try: + + Request # RetryInterval (seconds) Randomized Interval (seconds) + + 1 0.5 [0.25, 0.75] + 2 0.75 [0.375, 1.125] + 3 1.125 [0.562, 1.687] + 4 1.687 [0.8435, 2.53] + 5 2.53 [1.265, 3.795] + 6 3.795 [1.897, 5.692] + 7 5.692 [2.846, 8.538] + 8 8.538 [4.269, 12.807] + 9 12.807 [6.403, 19.210] + 10 19.210 backoff.Stop + +Note: Implementation is not thread-safe. +*/ +type ExponentialBackOff struct { + InitialInterval time.Duration + RandomizationFactor float64 + Multiplier float64 + MaxInterval time.Duration + // After MaxElapsedTime the ExponentialBackOff stops. + // It never stops if MaxElapsedTime == 0. + MaxElapsedTime time.Duration + Clock Clock + + currentInterval time.Duration + startTime time.Time +} + +// Clock is an interface that returns current time for BackOff. +type Clock interface { + Now() time.Time +} + +// Default values for ExponentialBackOff. +const ( + DefaultInitialInterval = 500 * time.Millisecond + DefaultRandomizationFactor = 0.5 + DefaultMultiplier = 1.5 + DefaultMaxInterval = 60 * time.Second + DefaultMaxElapsedTime = 15 * time.Minute +) + +// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. +func NewExponentialBackOff() *ExponentialBackOff { + b := &ExponentialBackOff{ + InitialInterval: DefaultInitialInterval, + RandomizationFactor: DefaultRandomizationFactor, + Multiplier: DefaultMultiplier, + MaxInterval: DefaultMaxInterval, + MaxElapsedTime: DefaultMaxElapsedTime, + Clock: SystemClock, + } + b.Reset() + return b +} + +type systemClock struct{} + +func (t systemClock) Now() time.Time { + return time.Now() +} + +// SystemClock implements Clock interface that uses time.Now(). +var SystemClock = systemClock{} + +// Reset the interval back to the initial retry interval and restarts the timer. +func (b *ExponentialBackOff) Reset() { + b.currentInterval = b.InitialInterval + b.startTime = b.Clock.Now() +} + +// NextBackOff calculates the next backoff interval using the formula: +// Randomized interval = RetryInterval +/- (RandomizationFactor * RetryInterval) +func (b *ExponentialBackOff) NextBackOff() time.Duration { + // Make sure we have not gone over the maximum elapsed time. + if b.MaxElapsedTime != 0 && b.GetElapsedTime() > b.MaxElapsedTime { + return Stop + } + defer b.incrementCurrentInterval() + return getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) +} + +// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance +// is created and is reset when Reset() is called. +// +// The elapsed time is computed using time.Now().UnixNano(). +func (b *ExponentialBackOff) GetElapsedTime() time.Duration { + return b.Clock.Now().Sub(b.startTime) +} + +// Increments the current interval by multiplying it with the multiplier. +func (b *ExponentialBackOff) incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { + b.currentInterval = b.MaxInterval + } else { + b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) + } +} + +// Returns a random value from the following interval: +// [randomizationFactor * currentInterval, randomizationFactor * currentInterval]. +func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { + var delta = randomizationFactor * float64(currentInterval) + var minInterval = float64(currentInterval) - delta + var maxInterval = float64(currentInterval) + delta + + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) +} diff --git a/vendor/github.com/cenkalti/backoff/exponential_test.go b/vendor/github.com/cenkalti/backoff/exponential_test.go new file mode 100644 index 000000000..11b95e4f6 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/exponential_test.go @@ -0,0 +1,108 @@ +package backoff + +import ( + "math" + "testing" + "time" +) + +func TestBackOff(t *testing.T) { + var ( + testInitialInterval = 500 * time.Millisecond + testRandomizationFactor = 0.1 + testMultiplier = 2.0 + testMaxInterval = 5 * time.Second + testMaxElapsedTime = 15 * time.Minute + ) + + exp := NewExponentialBackOff() + exp.InitialInterval = testInitialInterval + exp.RandomizationFactor = testRandomizationFactor + exp.Multiplier = testMultiplier + exp.MaxInterval = testMaxInterval + exp.MaxElapsedTime = testMaxElapsedTime + exp.Reset() + + var expectedResults = []time.Duration{500, 1000, 2000, 4000, 5000, 5000, 5000, 5000, 5000, 5000} + for i, d := range expectedResults { + expectedResults[i] = d * time.Millisecond + } + + for _, expected := range expectedResults { + assertEquals(t, expected, exp.currentInterval) + // Assert that the next backoff falls in the expected range. + var minInterval = expected - time.Duration(testRandomizationFactor*float64(expected)) + var maxInterval = expected + time.Duration(testRandomizationFactor*float64(expected)) + var actualInterval = exp.NextBackOff() + if !(minInterval <= actualInterval && actualInterval <= maxInterval) { + t.Error("error") + } + } +} + +func TestGetRandomizedInterval(t *testing.T) { + // 33% chance of being 1. + assertEquals(t, 1, getRandomValueFromInterval(0.5, 0, 2)) + assertEquals(t, 1, getRandomValueFromInterval(0.5, 0.33, 2)) + // 33% chance of being 2. + assertEquals(t, 2, getRandomValueFromInterval(0.5, 0.34, 2)) + assertEquals(t, 2, getRandomValueFromInterval(0.5, 0.66, 2)) + // 33% chance of being 3. + assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.67, 2)) + assertEquals(t, 3, getRandomValueFromInterval(0.5, 0.99, 2)) +} + +type TestClock struct { + i time.Duration + start time.Time +} + +func (c *TestClock) Now() time.Time { + t := c.start.Add(c.i) + c.i += time.Second + return t +} + +func TestGetElapsedTime(t *testing.T) { + var exp = NewExponentialBackOff() + exp.Clock = &TestClock{} + exp.Reset() + + var elapsedTime = exp.GetElapsedTime() + if elapsedTime != time.Second { + t.Errorf("elapsedTime=%d", elapsedTime) + } +} + +func TestMaxElapsedTime(t *testing.T) { + var exp = NewExponentialBackOff() + exp.Clock = &TestClock{start: time.Time{}.Add(10000 * time.Second)} + // Change the currentElapsedTime to be 0 ensuring that the elapsed time will be greater + // than the max elapsed time. + exp.startTime = time.Time{} + assertEquals(t, Stop, exp.NextBackOff()) +} + +func TestBackOffOverflow(t *testing.T) { + var ( + testInitialInterval time.Duration = math.MaxInt64 / 2 + testMaxInterval time.Duration = math.MaxInt64 + testMultiplier = 2.1 + ) + + exp := NewExponentialBackOff() + exp.InitialInterval = testInitialInterval + exp.Multiplier = testMultiplier + exp.MaxInterval = testMaxInterval + exp.Reset() + + exp.NextBackOff() + // Assert that when an overflow is possible the current varerval time.Duration is set to the max varerval time.Duration . + assertEquals(t, testMaxInterval, exp.currentInterval) +} + +func assertEquals(t *testing.T, expected, value time.Duration) { + if expected != value { + t.Errorf("got: %d, expected: %d", value, expected) + } +} diff --git a/vendor/github.com/cenkalti/backoff/retry.go b/vendor/github.com/cenkalti/backoff/retry.go new file mode 100644 index 000000000..f01f2bbd0 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/retry.go @@ -0,0 +1,46 @@ +package backoff + +import "time" + +// An Operation is executing by Retry() or RetryNotify(). +// The operation will be retried using a backoff policy if it returns an error. +type Operation func() error + +// Notify is a notify-on-error function. It receives an operation error and +// backoff delay if the operation failed (with an error). +// +// NOTE that if the backoff policy stated to stop retrying, +// the notify function isn't called. +type Notify func(error, time.Duration) + +// Retry the function f until it does not return error or BackOff stops. +// f is guaranteed to be run at least once. +// It is the caller's responsibility to reset b after Retry returns. +// +// Retry sleeps the goroutine for the duration returned by BackOff after a +// failed operation returns. +func Retry(o Operation, b BackOff) error { return RetryNotify(o, b, nil) } + +// RetryNotify calls notify function with the error and wait duration +// for each failed attempt before sleep. +func RetryNotify(operation Operation, b BackOff, notify Notify) error { + var err error + var next time.Duration + + b.Reset() + for { + if err = operation(); err == nil { + return nil + } + + if next = b.NextBackOff(); next == Stop { + return err + } + + if notify != nil { + notify(err, next) + } + + time.Sleep(next) + } +} diff --git a/vendor/github.com/cenkalti/backoff/retry_test.go b/vendor/github.com/cenkalti/backoff/retry_test.go new file mode 100644 index 000000000..c0d25ab76 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/retry_test.go @@ -0,0 +1,34 @@ +package backoff + +import ( + "errors" + "log" + "testing" +) + +func TestRetry(t *testing.T) { + const successOn = 3 + var i = 0 + + // This function is successfull on "successOn" calls. + f := func() error { + i++ + log.Printf("function is called %d. time\n", i) + + if i == successOn { + log.Println("OK") + return nil + } + + log.Println("error") + return errors.New("error") + } + + err := Retry(f, NewExponentialBackOff()) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if i != successOn { + t.Errorf("invalid number of retries: %d", i) + } +} diff --git a/vendor/github.com/cenkalti/backoff/ticker.go b/vendor/github.com/cenkalti/backoff/ticker.go new file mode 100644 index 000000000..7a5ff4ed1 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/ticker.go @@ -0,0 +1,79 @@ +package backoff + +import ( + "runtime" + "sync" + "time" +) + +// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff. +// +// Ticks will continue to arrive when the previous operation is still running, +// so operations that take a while to fail could run in quick succession. +type Ticker struct { + C <-chan time.Time + c chan time.Time + b BackOff + stop chan struct{} + stopOnce sync.Once +} + +// NewTicker returns a new Ticker containing a channel that will send the time at times +// specified by the BackOff argument. Ticker is guaranteed to tick at least once. +// The channel is closed when Stop method is called or BackOff stops. +func NewTicker(b BackOff) *Ticker { + c := make(chan time.Time) + t := &Ticker{ + C: c, + c: c, + b: b, + stop: make(chan struct{}), + } + go t.run() + runtime.SetFinalizer(t, (*Ticker).Stop) + return t +} + +// Stop turns off a ticker. After Stop, no more ticks will be sent. +func (t *Ticker) Stop() { + t.stopOnce.Do(func() { close(t.stop) }) +} + +func (t *Ticker) run() { + c := t.c + defer close(c) + t.b.Reset() + + // Ticker is guaranteed to tick at least once. + afterC := t.send(time.Now()) + + for { + if afterC == nil { + return + } + + select { + case tick := <-afterC: + afterC = t.send(tick) + case <-t.stop: + t.c = nil // Prevent future ticks from being sent to the channel. + return + } + } +} + +func (t *Ticker) send(tick time.Time) <-chan time.Time { + select { + case t.c <- tick: + case <-t.stop: + return nil + } + + next := t.b.NextBackOff() + if next == Stop { + t.Stop() + return nil + } + + return time.After(next) +} diff --git a/vendor/github.com/cenkalti/backoff/ticker_test.go b/vendor/github.com/cenkalti/backoff/ticker_test.go new file mode 100644 index 000000000..7c392df46 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/ticker_test.go @@ -0,0 +1,45 @@ +package backoff + +import ( + "errors" + "log" + "testing" +) + +func TestTicker(t *testing.T) { + const successOn = 3 + var i = 0 + + // This function is successfull on "successOn" calls. + f := func() error { + i++ + log.Printf("function is called %d. time\n", i) + + if i == successOn { + log.Println("OK") + return nil + } + + log.Println("error") + return errors.New("error") + } + + b := NewExponentialBackOff() + ticker := NewTicker(b) + + var err error + for _ = range ticker.C { + if err = f(); err != nil { + t.Log(err) + continue + } + + break + } + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if i != successOn { + t.Errorf("invalid number of retries: %d", i) + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/LICENSE b/vendor/github.com/zorkian/go-datadog-api/LICENSE new file mode 100644 index 000000000..f0903d29a --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2013 by authors and contributors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/zorkian/go-datadog-api/Makefile b/vendor/github.com/zorkian/go-datadog-api/Makefile new file mode 100644 index 000000000..b2256db53 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/Makefile @@ -0,0 +1,42 @@ +TEST?=. +VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr + +default: test + +# get dependencies +updatedeps: + go list ./... \ + | xargs go list -f '{{join .Deps "\n"}}' \ + | grep -v go-datadog-api\ + | grep -v '/internal/' \ + | sort -u \ + | xargs go get -f -u -v + +# test runs the unit tests and vets the code +test: + go test . $(TESTARGS) -v -timeout=30s -parallel=4 + @$(MAKE) vet + +# testacc runs acceptance tests +testacc: + go test integration/* -v $(TESTARGS) -timeout 90m + +# testrace runs the race checker +testrace: + go test -race $(TEST) $(TESTARGS) + +# vet runs the Go source code static analysis tool `vet` to find +# any common errors. +vet: + @go tool vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ + go get golang.org/x/tools/cmd/vet; \ + fi + @echo "go tool vet $(VETARGS) $(TEST) " + @go tool vet $(VETARGS) $(TEST) ; if [ $$? -eq 1 ]; then \ + echo ""; \ + echo "Vet found suspicious constructs. Please check the reported constructs"; \ + echo "and fix them if necessary before submitting the code for review."; \ + exit 1; \ + fi + +.PHONY: default test testacc updatedeps vet diff --git a/vendor/github.com/zorkian/go-datadog-api/README.md b/vendor/github.com/zorkian/go-datadog-api/README.md new file mode 100644 index 000000000..89cd20d10 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/README.md @@ -0,0 +1,69 @@ +[![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/zorkian/go-datadog-api) +[![Build +status](https://travis-ci.org/zorkian/go-datadog-api.svg)](https://travis-ci.org/zorkian/go-datadog-api) + +# Datadog API in Go + +Hi! + +This is a Go wrapper for the Datadog API. You should use this library if you need to interact +with the Datadog system. You can post metrics with it if you want, but this library is probably +mostly used for automating dashboards/alerting and retrieving data (events, etc). + +The source API documentation is here: + +## USAGE + +To use this project, include it in your code like: + +``` go + import "github.com/zorkian/go-datadog-api" +``` + +Then, you can work with it: + +``` go + client := datadog.NewClient("api key", "application key") + + dash, err := client.GetDashboard(10880) + if err != nil { + log.Fatalf("fatal: %s\n", err) + } + log.Printf("dashboard %d: %s\n", dash.Id, dash.Title) +``` + +That's all; it's pretty easy to use. Check out the Godoc link for the +available API methods and, if you can't find the one you need, +let us know (or patches welcome)! + +## DOCUMENTATION + +Please see: + +## BUGS/PROBLEMS/CONTRIBUTING + +There are certainly some, but presently no known major bugs. If you do +find something that doesn't work as expected, please file an issue on +Github: + + + +Thanks in advance! And, as always, patches welcome! + +## DEVELOPMENT + +* Get dependencies with `make updatedeps`. +* Run tests tests with `make test`. +* Integration tests can be run with `make testacc`. + +The acceptance tests require _DATADOG_API_KEY_ and _DATADOG_APP_KEY_ to be available +in your environment variables. + +*Warning: the integrations tests will create and remove real resources in your Datadog +account* + +## COPYRIGHT AND LICENSE + +Please see the LICENSE file for the included license information. + +Copyright 2013 by authors and contributors. diff --git a/vendor/github.com/zorkian/go-datadog-api/alerts.go b/vendor/github.com/zorkian/go-datadog-api/alerts.go new file mode 100644 index 000000000..c12296451 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/alerts.go @@ -0,0 +1,85 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "fmt" +) + +// Alert represents the data of an alert: a query that can fire and send a +// message to the users. +type Alert struct { + Id int `json:"id,omitempty"` + Creator int `json:"creator,omitempty"` + Query string `json:"query,omitempty"` + Name string `json:"name,omitempty"` + Message string `json:"message,omitempty"` + Silenced bool `json:"silenced,omitempty"` + NotifyNoData bool `json:"notify_no_data,omitempty"` + State string `json:"state,omitempty"` +} + +// reqAlerts receives a slice of all alerts. +type reqAlerts struct { + Alerts []Alert `json:"alerts,omitempty"` +} + +// CreateAlert adds a new alert to the system. This returns a pointer to an +// Alert so you can pass that to UpdateAlert later if needed. +func (self *Client) CreateAlert(alert *Alert) (*Alert, error) { + var out Alert + err := self.doJsonRequest("POST", "/v1/alert", alert, &out) + if err != nil { + return nil, err + } + return &out, nil +} + +// UpdateAlert takes an alert that was previously retrieved through some method +// and sends it back to the server. +func (self *Client) UpdateAlert(alert *Alert) error { + return self.doJsonRequest("PUT", fmt.Sprintf("/v1/alert/%d", alert.Id), + alert, nil) +} + +// GetAlert retrieves an alert by identifier. +func (self *Client) GetAlert(id int) (*Alert, error) { + var out Alert + err := self.doJsonRequest("GET", fmt.Sprintf("/v1/alert/%d", id), nil, &out) + if err != nil { + return nil, err + } + return &out, nil +} + +// DeleteAlert removes an alert from the system. +func (self *Client) DeleteAlert(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/alert/%d", id), + nil, nil) +} + +// GetAlerts returns a slice of all alerts. +func (self *Client) GetAlerts() ([]Alert, error) { + var out reqAlerts + err := self.doJsonRequest("GET", "/v1/alert", nil, &out) + if err != nil { + return nil, err + } + return out.Alerts, nil +} + +// MuteAlerts turns off alerting notifications. +func (self *Client) MuteAlerts() error { + return self.doJsonRequest("POST", "/v1/mute_alerts", nil, nil) +} + +// UnmuteAlerts turns on alerting notifications. +func (self *Client) UnmuteAlerts() error { + return self.doJsonRequest("POST", "/v1/unmute_alerts", nil, nil) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/comments.go b/vendor/github.com/zorkian/go-datadog-api/comments.go new file mode 100644 index 000000000..b16e3e96f --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/comments.go @@ -0,0 +1,65 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "fmt" +) + +// Comment is a special form of event that appears in a stream. +type Comment struct { + Id int `json:"id"` + RelatedId int `json:"related_event_id"` + Handle string `json:"handle"` + Message string `json:"message"` + Resource string `json:"resource"` + Url string `json:"url"` +} + +// reqComment is the container for receiving commenst. +type reqComment struct { + Comment Comment `json:"comment"` +} + +// CreateComment adds a new comment to the system. +func (self *Client) CreateComment(handle, message string) (*Comment, error) { + var out reqComment + comment := Comment{Handle: handle, Message: message} + err := self.doJsonRequest("POST", "/v1/comments", &comment, &out) + if err != nil { + return nil, err + } + return &out.Comment, nil +} + +// CreateRelatedComment adds a new comment, but lets you specify the related +// identifier for the comment. +func (self *Client) CreateRelatedComment(handle, message string, + relid int) (*Comment, error) { + var out reqComment + comment := Comment{Handle: handle, Message: message, RelatedId: relid} + err := self.doJsonRequest("POST", "/v1/comments", &comment, &out) + if err != nil { + return nil, err + } + return &out.Comment, nil +} + +// EditComment changes the message and possibly handle of a particular comment. +func (self *Client) EditComment(id int, handle, message string) error { + comment := Comment{Handle: handle, Message: message} + return self.doJsonRequest("PUT", fmt.Sprintf("/v1/comments/%d", id), + &comment, nil) +} + +// DeleteComment does exactly what you expect. +func (self *Client) DeleteComment(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/comments/%d", id), + nil, nil) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/dashboards.go b/vendor/github.com/zorkian/go-datadog-api/dashboards.go new file mode 100644 index 000000000..c21a34ed4 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/dashboards.go @@ -0,0 +1,107 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "fmt" +) + +// Graph represents a graph that might exist on a dashboard. +type Graph struct { + Title string `json:"title"` + Events []struct{} `json:"events"` + Definition struct { + Viz string `json:"viz"` + Requests []struct { + Query string `json:"q"` + Stacked bool `json:"stacked"` + } `json:"requests"` + } `json:"definition"` +} + +// Template variable represents a template variable that might exist on a dashboard +type TemplateVariable struct { + Name string `json:"name"` + Prefix string `json:"prefix"` + Default string `json:"default"` +} + +// Dashboard represents a user created dashboard. This is the full dashboard +// struct when we load a dashboard in detail. +type Dashboard struct { + Id int `json:"id"` + Description string `json:"description"` + Title string `json:"title"` + Graphs []Graph `json:"graphs"` + TemplateVariables []TemplateVariable `json:"template_variables,omitempty"` +} + +// DashboardLite represents a user created dashboard. This is the mini +// struct when we load the summaries. +type DashboardLite struct { + Id int `json:"id,string"` // TODO: Remove ',string'. + Resource string `json:"resource"` + Description string `json:"description"` + Title string `json:"title"` +} + +// reqGetDashboards from /api/v1/dash +type reqGetDashboards struct { + Dashboards []DashboardLite `json:"dashes"` +} + +// reqGetDashboard from /api/v1/dash/:dashboard_id +type reqGetDashboard struct { + Resource string `json:"resource"` + Url string `json:"url"` + Dashboard Dashboard `json:"dash"` +} + +// GetDashboard returns a single dashboard created on this account. +func (self *Client) GetDashboard(id int) (*Dashboard, error) { + var out reqGetDashboard + err := self.doJsonRequest("GET", fmt.Sprintf("/v1/dash/%d", id), nil, &out) + if err != nil { + return nil, err + } + return &out.Dashboard, nil +} + +// GetDashboards returns a list of all dashboards created on this account. +func (self *Client) GetDashboards() ([]DashboardLite, error) { + var out reqGetDashboards + err := self.doJsonRequest("GET", "/v1/dash", nil, &out) + if err != nil { + return nil, err + } + return out.Dashboards, nil +} + +// DeleteDashboard deletes a dashboard by the identifier. +func (self *Client) DeleteDashboard(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/dash/%d", id), nil, nil) +} + +// CreateDashboard creates a new dashboard when given a Dashboard struct. Note +// that the Id, Resource, Url and similar elements are not used in creation. +func (self *Client) CreateDashboard(dash *Dashboard) (*Dashboard, error) { + var out reqGetDashboard + err := self.doJsonRequest("POST", "/v1/dash", dash, &out) + if err != nil { + return nil, err + } + return &out.Dashboard, nil +} + +// UpdateDashboard in essence takes a Dashboard struct and persists it back to +// the server. Use this if you've updated your local and need to push it back. +func (self *Client) UpdateDashboard(dash *Dashboard) error { + return self.doJsonRequest("PUT", fmt.Sprintf("/v1/dash/%d", dash.Id), + dash, nil) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/downtimes.go b/vendor/github.com/zorkian/go-datadog-api/downtimes.go new file mode 100644 index 000000000..6ff16ab57 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/downtimes.go @@ -0,0 +1,83 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "fmt" +) + +type Recurrence struct { + Period int `json:"period,omitempty"` + Type string `json:"type,omitempty"` + UntilDate int `json:"until_date,omitempty"` + UntilOccurrences int `json:"until_occurrences,omitempty"` + WeekDays []string `json:"week_days,omitempty"` +} + +type Downtime struct { + Active bool `json:"active,omitempty"` + Canceled int `json:"canceled,omitempty"` + Disabled bool `json:"disabled,omitempty"` + End int `json:"end,omitempty"` + Id int `json:"id,omitempty"` + Message string `json:"message,omitempty"` + Recurrence *Recurrence `json:"recurrence,omitempty"` + Scope []string `json:"scope,omitempty"` + Start int `json:"start,omitempty"` +} + +// reqDowntimes retrieves a slice of all Downtimes. +type reqDowntimes struct { + Downtimes []Downtime `json:"downtimes,omitempty"` +} + +// CreateDowntime adds a new downtme to the system. This returns a pointer +// to a Downtime so you can pass that to UpdateDowntime or CancelDowntime +// later if needed. +func (self *Client) CreateDowntime(downtime *Downtime) (*Downtime, error) { + var out Downtime + err := self.doJsonRequest("POST", "/v1/downtime", downtime, &out) + if err != nil { + return nil, err + } + return &out, nil +} + +// UpdateDowntime takes a downtime that was previously retrieved through some method +// and sends it back to the server. +func (self *Client) UpdateDowntime(downtime *Downtime) error { + return self.doJsonRequest("PUT", fmt.Sprintf("/v1/downtime/%d", downtime.Id), + downtime, nil) +} + +// Getdowntime retrieves an downtime by identifier. +func (self *Client) GetDowntime(id int) (*Downtime, error) { + var out Downtime + err := self.doJsonRequest("GET", fmt.Sprintf("/v1/downtime/%d", id), nil, &out) + if err != nil { + return nil, err + } + return &out, nil +} + +// DeleteDowntime removes an downtime from the system. +func (self *Client) DeleteDowntime(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/downtime/%d", id), + nil, nil) +} + +// GetDowntimes returns a slice of all downtimes. +func (self *Client) GetDowntimes() ([]Downtime, error) { + var out reqDowntimes + err := self.doJsonRequest("GET", "/v1/downtime", nil, &out.Downtimes) + if err != nil { + return nil, err + } + return out.Downtimes, nil +} diff --git a/vendor/github.com/zorkian/go-datadog-api/events.go b/vendor/github.com/zorkian/go-datadog-api/events.go new file mode 100644 index 000000000..2278a120a --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/events.go @@ -0,0 +1,89 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "fmt" + "net/url" + "strconv" +) + +// Event is a single event. If this is being used to post an event, then not +// all fields will be filled out. +type Event struct { + Id int `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Text string `json:"text,omitempty"` + Time int `json:"date_happened,omitempty"` // UNIX time. + Priority string `json:"priority,omitempty"` + AlertType string `json:"alert_type,omitempty"` + Host string `json:"host,omitempty"` + Aggregation string `json:"aggregation_key,omitempty"` + SourceType string `json:"source_type,omitempty"` + Tags []string `json:"tags,omitempty"` + Url string `json:"url,omitempty"` + Resource string `json:"resource,omitempty"` +} + +// reqGetEvent is the container for receiving a single event. +type reqGetEvent struct { + Event Event `json:"event,omitempty"` +} + +// reqGetEvents is for returning many events. +type reqGetEvents struct { + Events []Event `json:"events,omitempty"` +} + +// PostEvent takes as input an event and then posts it to the server. +func (self *Client) PostEvent(event *Event) (*Event, error) { + var out reqGetEvent + err := self.doJsonRequest("POST", "/v1/events", event, &out) + if err != nil { + return nil, err + } + return &out.Event, nil +} + +// GetEvent gets a single event given an identifier. +func (self *Client) GetEvent(id int) (*Event, error) { + var out reqGetEvent + err := self.doJsonRequest("GET", fmt.Sprintf("/v1/events/%d", id), nil, &out) + if err != nil { + return nil, err + } + return &out.Event, nil +} + +// QueryEvents returns a slice of events from the query stream. +func (self *Client) GetEvents(start, end int, + priority, sources, tags string) ([]Event, error) { + // Since this is a GET request, we need to build a query string. + vals := url.Values{} + vals.Add("start", strconv.Itoa(start)) + vals.Add("end", strconv.Itoa(end)) + if priority != "" { + vals.Add("priority", priority) + } + if sources != "" { + vals.Add("sources", sources) + } + if tags != "" { + vals.Add("tags", tags) + } + + // Now the request and response. + var out reqGetEvents + err := self.doJsonRequest("GET", + fmt.Sprintf("/v1/events?%s", vals.Encode()), nil, &out) + if err != nil { + return nil, err + } + return out.Events, nil +} diff --git a/vendor/github.com/zorkian/go-datadog-api/integration/dashboards_test.go b/vendor/github.com/zorkian/go-datadog-api/integration/dashboards_test.go new file mode 100644 index 000000000..272a5e161 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/integration/dashboards_test.go @@ -0,0 +1,138 @@ +package integration + +import ( + "github.com/zorkian/go-datadog-api" + "testing" +) + +func init() { + client = initTest() +} + +func TestCreateAndDeleteDashboard(t *testing.T) { + expected := getTestDashboard() + // create the dashboard and compare it + actual, err := client.CreateDashboard(expected) + if err != nil { + t.Fatalf("Creating a dashboard failed when it shouldn't. (%s)", err) + } + + defer cleanUpDashboard(t, actual.Id) + + assertDashboardEquals(t, actual, expected) + + // now try to fetch it freshly and compare it again + actual, err = client.GetDashboard(actual.Id) + if err != nil { + t.Fatalf("Retrieving a dashboard failed when it shouldn't. (%s)", err) + } + assertDashboardEquals(t, actual, expected) + +} + +func TestUpdateDashboard(t *testing.T) { + expected := getTestDashboard() + board, err := client.CreateDashboard(expected) + if err != nil { + t.Fatalf("Creating a dashboard failed when it shouldn't. (%s)", err) + } + + defer cleanUpDashboard(t, board.Id) + board.Title = "___New-Test-Board___" + + if err := client.UpdateDashboard(board); err != nil { + t.Fatalf("Updating a dashboard failed when it shouldn't: %s", err) + } + + actual, err := client.GetDashboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a dashboard failed when it shouldn't: %s", err) + } + + assertDashboardEquals(t, actual, board) +} + +func TestGetDashboards(t *testing.T) { + boards, err := client.GetDashboards() + if err != nil { + t.Fatalf("Retrieving dashboards failed when it shouldn't: %s", err) + } + + num := len(boards) + board := createTestDashboard(t) + defer cleanUpDashboard(t, board.Id) + + boards, err = client.GetDashboards() + if err != nil { + t.Fatalf("Retrieving dashboards failed when it shouldn't: %s", err) + } + + if num+1 != len(boards) { + t.Fatalf("Number of dashboards didn't match expected: %d != %d", len(boards), num+1) + } +} + +func getTestDashboard() *datadog.Dashboard { + return &datadog.Dashboard{ + Title: "___Test-Board___", + Description: "Testboard description", + TemplateVariables: []datadog.TemplateVariable{}, + Graphs: createGraph(), + } +} + +func createTestDashboard(t *testing.T) *datadog.Dashboard { + board := getTestDashboard() + board, err := client.CreateDashboard(board) + if err != nil { + t.Fatalf("Creating a dashboard failed when it shouldn't: %s", err) + } + + return board +} + +func cleanUpDashboard(t *testing.T, id int) { + if err := client.DeleteDashboard(id); err != nil { + t.Fatalf("Deleting a dashboard failed when it shouldn't. Manual cleanup needed. (%s)", err) + } + + deletedBoard, err := client.GetDashboard(id) + if deletedBoard != nil { + t.Fatal("Dashboard hasn't been deleted when it should have been. Manual cleanup needed.") + } + + if err == nil { + t.Fatal("Fetching deleted dashboard didn't lead to an error. Manual cleanup needed.") + } +} + +type TestGraphDefintionRequests struct { + Query string `json:"q"` + Stacked bool `json:"stacked"` +} + +func createGraph() []datadog.Graph { + graphDefinition := datadog.Graph{}.Definition + graphDefinition.Viz = "timeseries" + r := datadog.Graph{}.Definition.Requests + graphDefinition.Requests = append(r, TestGraphDefintionRequests{Query: "avg:system.mem.free{*}", Stacked: false}) + graph := datadog.Graph{Title: "Mandatory graph", Definition: graphDefinition} + graphs := []datadog.Graph{} + graphs = append(graphs, graph) + return graphs +} + +func assertDashboardEquals(t *testing.T, actual, expected *datadog.Dashboard) { + if actual.Title != expected.Title { + t.Errorf("Dashboard title does not match: %s != %s", actual.Title, expected.Title) + } + if actual.Description != expected.Description { + t.Errorf("Dashboard description does not match: %s != %s", actual.Description, expected.Description) + } + if len(actual.Graphs) != len(expected.Graphs) { + t.Errorf("Number of Dashboard graphs does not match: %d != %d", len(actual.Graphs), len(expected.Graphs)) + } + if len(actual.TemplateVariables) != len(expected.TemplateVariables) { + t.Errorf("Number of Dashboard template variables does not match: %d != %d", len(actual.TemplateVariables), len(expected.TemplateVariables)) + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/integration/downtime_test.go b/vendor/github.com/zorkian/go-datadog-api/integration/downtime_test.go new file mode 100644 index 000000000..026cd5857 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/integration/downtime_test.go @@ -0,0 +1,110 @@ +package integration + +import ( + "github.com/stretchr/testify/assert" + "github.com/zorkian/go-datadog-api" + "testing" +) + +func init() { + client = initTest() +} + +func TestCreateAndDeleteDowntime(t *testing.T) { + expected := getTestDowntime() + // create the downtime and compare it + actual := createTestDowntime(t) + defer cleanUpDowntime(t, actual.Id) + + // Set ID of our original struct to zero we we can easily compare the results + expected.Id = actual.Id + assert.Equal(t, expected, actual) + + actual, err := client.GetDowntime(actual.Id) + if err != nil { + t.Fatalf("Retrieving a downtime failed when it shouldn't: (%s)", err) + } + assert.Equal(t, expected, actual) +} + +func TestUpdateDowntime(t *testing.T) { + + downtime := createTestDowntime(t) + + downtime.Scope = []string{"env:downtime_test", "env:downtime_test2"} + defer cleanUpDowntime(t, downtime.Id) + + if err := client.UpdateDowntime(downtime); err != nil { + t.Fatalf("Updating a downtime failed when it shouldn't: %s", err) + } + + actual, err := client.GetDowntime(downtime.Id) + if err != nil { + t.Fatalf("Retrieving a downtime failed when it shouldn't: %s", err) + } + + assert.Equal(t, downtime, actual) + +} + +func TestGetDowntime(t *testing.T) { + downtimes, err := client.GetDowntimes() + if err != nil { + t.Fatalf("Retrieving downtimes failed when it shouldn't: %s", err) + } + num := len(downtimes) + + downtime := createTestDowntime(t) + defer cleanUpDowntime(t, downtime.Id) + + downtimes, err = client.GetDowntimes() + if err != nil { + t.Fatalf("Retrieving downtimes failed when it shouldn't: %s", err) + } + + if num+1 != len(downtimes) { + t.Fatalf("Number of downtimes didn't match expected: %d != %d", len(downtimes), num+1) + } +} + +func getTestDowntime() *datadog.Downtime { + + r := &datadog.Recurrence{ + Type: "weeks", + Period: 1, + WeekDays: []string{"Mon", "Tue", "Wed", "Thu", "Fri"}, + } + + return &datadog.Downtime{ + Message: "Test downtime message", + Scope: []string{"env:downtime_test"}, + Start: 1577836800, + End: 1577840400, + Recurrence: r, + } +} + +func createTestDowntime(t *testing.T) *datadog.Downtime { + downtime := getTestDowntime() + downtime, err := client.CreateDowntime(downtime) + if err != nil { + t.Fatalf("Creating a downtime failed when it shouldn't: %s", err) + } + + return downtime +} + +func cleanUpDowntime(t *testing.T, id int) { + if err := client.DeleteDowntime(id); err != nil { + t.Fatalf("Deleting a downtime failed when it shouldn't. Manual cleanup needed. (%s)", err) + } + + deletedDowntime, err := client.GetDowntime(id) + if deletedDowntime != nil && deletedDowntime.Canceled == 0 { + t.Fatal("Downtime hasn't been deleted when it should have been. Manual cleanup needed.") + } + + if err == nil && deletedDowntime.Canceled == 0 { + t.Fatal("Fetching deleted downtime didn't lead to an error and downtime Canceled not set.") + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/integration/monitors_test.go b/vendor/github.com/zorkian/go-datadog-api/integration/monitors_test.go new file mode 100644 index 000000000..527fc63fa --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/integration/monitors_test.go @@ -0,0 +1,152 @@ +package integration + +import ( + "github.com/stretchr/testify/assert" + "github.com/zorkian/go-datadog-api" + "testing" +) + +func init() { + client = initTest() +} + +func TestCreateAndDeleteMonitor(t *testing.T) { + expected := getTestMonitor() + // create the monitor and compare it + actual := createTestMonitor(t) + defer cleanUpMonitor(t, actual.Id) + + // Set ID of our original struct to zero we we can easily compare the results + expected.Id = actual.Id + assert.Equal(t, expected, actual) + + actual, err := client.GetMonitor(actual.Id) + if err != nil { + t.Fatalf("Retrieving a monitor failed when it shouldn't: (%s)", err) + } + assert.Equal(t, expected, actual) +} + +func TestUpdateMonitor(t *testing.T) { + + monitor := createTestMonitor(t) + defer cleanUpMonitor(t, monitor.Id) + + monitor.Name = "___New-Test-Monitor___" + if err := client.UpdateMonitor(monitor); err != nil { + t.Fatalf("Updating a monitor failed when it shouldn't: %s", err) + } + + actual, err := client.GetMonitor(monitor.Id) + if err != nil { + t.Fatalf("Retrieving a monitor failed when it shouldn't: %s", err) + } + + assert.Equal(t, monitor, actual) + +} + +func TestGetMonitor(t *testing.T) { + monitors, err := client.GetMonitors() + if err != nil { + t.Fatalf("Retrieving monitors failed when it shouldn't: %s", err) + } + num := len(monitors) + + monitor := createTestMonitor(t) + defer cleanUpMonitor(t, monitor.Id) + + monitors, err = client.GetMonitors() + if err != nil { + t.Fatalf("Retrieving monitors failed when it shouldn't: %s", err) + } + + if num+1 != len(monitors) { + t.Fatalf("Number of monitors didn't match expected: %d != %d", len(monitors), num+1) + } +} + +func TestMuteUnmuteMonitor(t *testing.T) { + monitor := createTestMonitor(t) + defer cleanUpMonitor(t, monitor.Id) + + // Mute + err := client.MuteMonitor(monitor.Id) + if err != nil { + t.Fatalf("Failed to mute monitor") + + } + + monitor, err = client.GetMonitor(monitor.Id) + if err != nil { + t.Fatalf("Retrieving monitors failed when it shouldn't: %s", err) + } + + // Mute without options will result in monitor.Options.Silenced + // to have a key of "*" with value 0 + assert.Equal(t, 0, monitor.Options.Silenced["*"]) + + // Unmute + err = client.UnmuteMonitor(monitor.Id) + if err != nil { + t.Fatalf("Failed to unmute monitor") + } + + // Update remote state + monitor, err = client.GetMonitor(monitor.Id) + if err != nil { + t.Fatalf("Retrieving monitors failed when it shouldn't: %s", err) + } + + // Assert this map is empty + assert.Equal(t, 0, len(monitor.Options.Silenced)) +} + +/* + Testing of global mute and unmuting has not been added for following reasons: + * Disabling and enabling of global monitoring does an @all mention which is noisy + * It exposes risk to users that run integration tests in their main account + * There is no endpoint to verify success +*/ + +func getTestMonitor() *datadog.Monitor { + + o := datadog.Options{ + NotifyNoData: true, + NoDataTimeframe: 60, + Silenced: map[string]int{}, + } + + return &datadog.Monitor{ + Message: "Test message", + Query: "avg(last_15m):avg:system.disk.in_use{*} by {host,device} > 0.8", + Name: "Test monitor", + Options: o, + Type: "metric alert", + } +} + +func createTestMonitor(t *testing.T) *datadog.Monitor { + monitor := getTestMonitor() + monitor, err := client.CreateMonitor(monitor) + if err != nil { + t.Fatalf("Creating a monitor failed when it shouldn't: %s", err) + } + + return monitor +} + +func cleanUpMonitor(t *testing.T, id int) { + if err := client.DeleteMonitor(id); err != nil { + t.Fatalf("Deleting a monitor failed when it shouldn't. Manual cleanup needed. (%s)", err) + } + + deletedMonitor, err := client.GetMonitor(id) + if deletedMonitor != nil { + t.Fatal("Monitor hasn't been deleted when it should have been. Manual cleanup needed.") + } + + if err == nil { + t.Fatal("Fetching deleted monitor didn't lead to an error.") + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/integration/screen_widgets_test.go b/vendor/github.com/zorkian/go-datadog-api/integration/screen_widgets_test.go new file mode 100644 index 000000000..6611ab090 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/integration/screen_widgets_test.go @@ -0,0 +1,766 @@ +package integration + +import ( + "testing" + + "github.com/zorkian/go-datadog-api" +) + +func TestAlertValueWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.AlertValueWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.TextSize = "auto" + expected.Precision = 2 + expected.AlertId = 1 + expected.Type = "alert_value" + expected.Unit = "auto" + expected.AddTimeframe = false + + w := datadog.Widget{AlertValueWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].AlertValueWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "text_size", actualWidget.TextSize, expected.TextSize) + assertEquals(t, "precision", actualWidget.Precision, expected.Precision) + assertEquals(t, "alert_id", actualWidget.AlertId, expected.AlertId) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "unit", actualWidget.Unit, expected.Unit) + assertEquals(t, "add_timeframe", actualWidget.AddTimeframe, expected.AddTimeframe) +} + +func TestChangeWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.ChangeWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Aggregator = "min" + expected.TileDef = datadog.TileDef{} + + w := datadog.Widget{ChangeWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].ChangeWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "aggregator", actualWidget.Aggregator, expected.Aggregator) + assertTileDefEquals(t, actualWidget.TileDef, expected.TileDef) +} + +func TestGraphWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.GraphWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Timeframe = "1d" + expected.Type = "alert_graph" + expected.Legend = true + expected.LegendSize = 5 + expected.TileDef = datadog.TileDef{} + + w := datadog.Widget{GraphWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].GraphWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "legend", actualWidget.Legend, expected.Legend) + assertEquals(t, "legend_size", actualWidget.LegendSize, expected.LegendSize) + assertTileDefEquals(t, actualWidget.TileDef, expected.TileDef) +} + +func TestEventTimelineWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.EventTimelineWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Query = "avg:system.load.1{foo} by {bar}" + expected.Timeframe = "1d" + expected.Type = "alert_graph" + + w := datadog.Widget{EventTimelineWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].EventTimelineWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "query", actualWidget.Query, expected.Query) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) +} + +func TestAlertGraphWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.AlertGraphWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.VizType = "" + expected.Timeframe = "1d" + expected.AddTimeframe = false + expected.AlertId = 1 + expected.Type = "alert_graph" + + w := datadog.Widget{AlertGraphWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].AlertGraphWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "viz_type", actualWidget.VizType, expected.VizType) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "add_timeframe", actualWidget.AddTimeframe, expected.AddTimeframe) + assertEquals(t, "alert_id", actualWidget.AlertId, expected.AlertId) +} + +func TestHostMapWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.HostMapWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Type = "check_status" + expected.Query = "avg:system.load.1{foo} by {bar}" + expected.Timeframe = "1d" + expected.Legend = true + expected.LegendSize = 5 + expected.TileDef = datadog.TileDef{} + + w := datadog.Widget{HostMapWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].HostMapWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "query", actualWidget.Query, expected.Query) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "query", actualWidget.Query, expected.Query) + assertEquals(t, "legend", actualWidget.Legend, expected.Legend) + assertEquals(t, "legend_size", actualWidget.LegendSize, expected.LegendSize) + assertTileDefEquals(t, actualWidget.TileDef, expected.TileDef) +} + +func TestCheckStatusWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.CheckStatusWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Type = "check_status" + expected.Tags = "foo" + expected.Timeframe = "1d" + expected.Timeframe = "1d" + expected.Check = "datadog.agent.up" + expected.Group = "foo" + expected.Grouping = "check" + + w := datadog.Widget{CheckStatusWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].CheckStatusWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "tags", actualWidget.Tags, expected.Tags) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "check", actualWidget.Check, expected.Check) + assertEquals(t, "group", actualWidget.Group, expected.Group) + assertEquals(t, "grouping", actualWidget.Grouping, expected.Grouping) +} + +func TestIFrameWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.IFrameWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Url = "http://www.example.com" + expected.Type = "iframe" + + w := datadog.Widget{IFrameWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].IFrameWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "url", actualWidget.Url, expected.Url) + assertEquals(t, "type", actualWidget.Type, expected.Type) +} + +func TestNoteWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.NoteWidget + + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.TitleText = "foo" + expected.TitleAlign = "center" + expected.TitleSize = 1 + expected.Title = true + expected.Color = "green" + expected.FontSize = 5 + expected.RefreshEvery = 60 + expected.TickPos = "foo" + expected.TickEdge = "bar" + expected.Html = "baz" + expected.Tick = false + expected.Note = "quz" + expected.AutoRefresh = false + + w := datadog.Widget{NoteWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].NoteWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "color", actualWidget.Color, expected.Color) + assertEquals(t, "front_size", actualWidget.FontSize, expected.FontSize) + assertEquals(t, "refresh_every", actualWidget.RefreshEvery, expected.RefreshEvery) + assertEquals(t, "tick_pos", actualWidget.TickPos, expected.TickPos) + assertEquals(t, "tick_edge", actualWidget.TickEdge, expected.TickEdge) + assertEquals(t, "tick", actualWidget.Tick, expected.Tick) + assertEquals(t, "html", actualWidget.Html, expected.Html) + assertEquals(t, "note", actualWidget.Note, expected.Note) + assertEquals(t, "auto_refresh", actualWidget.AutoRefresh, expected.AutoRefresh) +} + +func TestToplistWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.ToplistWidget + expected.X = 1 + expected.Y = 1 + expected.Width = 5 + expected.Height = 5 + expected.Type = "toplist" + expected.TitleText = "foo" + expected.TitleSize.Auto = false + expected.TitleSize.Size = 5 + expected.TitleAlign = "center" + expected.Title = false + expected.Timeframe = "5m" + expected.Legend = false + expected.LegendSize = 5 + + w := datadog.Widget{ToplistWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].ToplistWidget + + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "legend", actualWidget.Legend, expected.Legend) + assertEquals(t, "legend_size", actualWidget.LegendSize, expected.LegendSize) +} + +func TestEventSteamWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.EventStreamWidget + expected.EventSize = "1" + expected.Width = 1 + expected.Height = 1 + expected.X = 1 + expected.Y = 1 + expected.Query = "foo" + expected.Timeframe = "5w" + expected.Title = false + expected.TitleAlign = "center" + expected.TitleSize.Auto = false + expected.TitleSize.Size = 5 + expected.TitleText = "bar" + expected.Type = "baz" + + w := datadog.Widget{EventStreamWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].EventStreamWidget + + assertEquals(t, "event_size", actualWidget.EventSize, expected.EventSize) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "query", actualWidget.Query, expected.Query) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "type", actualWidget.Type, expected.Type) +} + +func TestImageWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.ImageWidget + + expected.Width = 1 + expected.Height = 1 + expected.X = 1 + expected.Y = 1 + expected.Title = false + expected.TitleAlign = "center" + expected.TitleSize.Auto = false + expected.TitleSize.Size = 5 + expected.TitleText = "bar" + expected.Type = "baz" + expected.Url = "qux" + expected.Sizing = "quuz" + + w := datadog.Widget{ImageWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].ImageWidget + + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "title_align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title_size", actualWidget.TitleSize, expected.TitleSize) + assertEquals(t, "title_text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "url", actualWidget.Url, expected.Url) + assertEquals(t, "sizing", actualWidget.Sizing, expected.Sizing) +} + +func TestFreeTextWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.FreeTextWidget + + expected.X = 1 + expected.Y = 1 + expected.Height = 10 + expected.Width = 10 + expected.Text = "Test" + expected.FontSize = "16" + expected.TextAlign = "center" + + w := datadog.Widget{FreeTextWidget: expected} + + board.Widgets = append(board.Widgets, w) + + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].FreeTextWidget + + assertEquals(t, "font-size", actualWidget.FontSize, expected.FontSize) + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "text", actualWidget.Text, expected.Text) + assertEquals(t, "text-align", actualWidget.TextAlign, expected.TextAlign) + assertEquals(t, "type", actualWidget.Type, expected.Type) +} + +func TestTimeseriesWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.TimeseriesWidget + expected.X = 1 + expected.Y = 1 + expected.Width = 20 + expected.Height = 30 + expected.Title = true + expected.TitleAlign = "centre" + expected.TitleSize = datadog.TextSize{Size: 16} + expected.TitleText = "Test" + expected.Timeframe = "1m" + + w := datadog.Widget{TimeseriesWidget: expected} + + board.Widgets = append(board.Widgets, w) + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].TimeseriesWidget + + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "title-align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title-size.size", actualWidget.TitleSize.Size, expected.TitleSize.Size) + assertEquals(t, "title-size.auto", actualWidget.TitleSize.Auto, expected.TitleSize.Auto) + assertEquals(t, "title-text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "legend", actualWidget.Legend, expected.Legend) + assertTileDefEquals(t, actualWidget.TileDef, expected.TileDef) +} + +func TestQueryValueWidget(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + expected := datadog.Widget{}.QueryValueWidget + expected.X = 1 + expected.Y = 1 + expected.Width = 20 + expected.Height = 30 + expected.Title = true + expected.TitleAlign = "centre" + expected.TitleSize = datadog.TextSize{Size: 16} + expected.TitleText = "Test" + expected.Timeframe = "1m" + expected.TimeframeAggregator = "sum" + expected.Aggregator = "min" + expected.Query = "docker.containers.running" + expected.MetricType = "standard" + /* TODO: add test for conditional formats + "conditional_formats": [{ + "comparator": ">", + "color": "white_on_red", + "custom_bg_color": null, + "value": 1, + "invert": false, + "custom_fg_color": null}], + */ + expected.IsValidQuery = true + expected.ResultCalcFunc = "raw" + expected.Aggregator = "avg" + expected.CalcFunc = "raw" + + w := datadog.Widget{QueryValueWidget: expected} + + board.Widgets = append(board.Widgets, w) + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed: %s", err) + } + + actualWidget := actual.Widgets[0].QueryValueWidget + + assertEquals(t, "height", actualWidget.Height, expected.Height) + assertEquals(t, "width", actualWidget.Width, expected.Width) + assertEquals(t, "x", actualWidget.X, expected.X) + assertEquals(t, "y", actualWidget.Y, expected.Y) + assertEquals(t, "title", actualWidget.Title, expected.Title) + assertEquals(t, "title-align", actualWidget.TitleAlign, expected.TitleAlign) + assertEquals(t, "title-size.size", actualWidget.TitleSize.Size, expected.TitleSize.Size) + assertEquals(t, "title-size.auto", actualWidget.TitleSize.Auto, expected.TitleSize.Auto) + assertEquals(t, "title-text", actualWidget.TitleText, expected.TitleText) + assertEquals(t, "type", actualWidget.Type, expected.Type) + assertEquals(t, "timeframe", actualWidget.Timeframe, expected.Timeframe) + assertEquals(t, "timeframe-aggregator", actualWidget.TimeframeAggregator, expected.TimeframeAggregator) + assertEquals(t, "aggregator", actualWidget.Aggregator, expected.Aggregator) + assertEquals(t, "query", actualWidget.Query, expected.Query) + assertEquals(t, "is_valid_query", actualWidget.IsValidQuery, expected.IsValidQuery) + assertEquals(t, "res_calc_func", actualWidget.ResultCalcFunc, expected.ResultCalcFunc) + assertEquals(t, "aggr", actualWidget.Aggregator, expected.Aggregator) +} + +func assertTileDefEquals(t *testing.T, actual datadog.TileDef, expected datadog.TileDef) { + assertEquals(t, "num-events", len(actual.Events), len(expected.Events)) + assertEquals(t, "num-requests", len(actual.Requests), len(expected.Requests)) + assertEquals(t, "viz", actual.Viz, expected.Viz) + + for i, event := range actual.Events { + assertEquals(t, "event-query", event.Query, expected.Events[i].Query) + } + + for i, request := range actual.Requests { + assertEquals(t, "request-query", request.Query, expected.Requests[i].Query) + assertEquals(t, "request-type", request.Type, expected.Requests[i].Type) + } +} + +func assertEquals(t *testing.T, attribute string, a, b interface{}) { + if a != b { + t.Errorf("The two %s values '%v' and '%v' are not equal", attribute, a, b) + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/integration/screenboards_test.go b/vendor/github.com/zorkian/go-datadog-api/integration/screenboards_test.go new file mode 100644 index 000000000..654d45f18 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/integration/screenboards_test.go @@ -0,0 +1,143 @@ +package integration + +import ( + "github.com/zorkian/go-datadog-api" + "testing" +) + +func init() { + client = initTest() +} + +func TestCreateAndDeleteScreenboard(t *testing.T) { + expected := getTestScreenboard() + // create the screenboard and compare it + actual, err := client.CreateScreenboard(expected) + if err != nil { + t.Fatalf("Creating a screenboard failed when it shouldn't. (%s)", err) + } + + defer cleanUpScreenboard(t, actual.Id) + + assertScreenboardEquals(t, actual, expected) + + // now try to fetch it freshly and compare it again + actual, err = client.GetScreenboard(actual.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed when it shouldn't. (%s)", err) + } + + assertScreenboardEquals(t, actual, expected) + +} + +func TestShareAndRevokeScreenboard(t *testing.T) { + expected := getTestScreenboard() + // create the screenboard + actual, err := client.CreateScreenboard(expected) + if err != nil { + t.Fatalf("Creating a screenboard failed when it shouldn't: %s", err) + } + + defer cleanUpScreenboard(t, actual.Id) + + // share screenboard and verify it was shared + var response datadog.ScreenShareResponse + err = client.ShareScreenboard(actual.Id, &response) + if err != nil { + t.Fatalf("Failed to share screenboard: %s", err) + } + + // revoke screenboard + err = client.RevokeScreenboard(actual.Id) + if err != nil { + t.Fatalf("Failed to revoke sharing of screenboard: %s", err) + } +} + +func TestUpdateScreenboard(t *testing.T) { + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + board.Title = "___New-Test-Board___" + if err := client.UpdateScreenboard(board); err != nil { + t.Fatalf("Updating a screenboard failed when it shouldn't: %s", err) + } + + actual, err := client.GetScreenboard(board.Id) + if err != nil { + t.Fatalf("Retrieving a screenboard failed when it shouldn't: %s", err) + } + + assertScreenboardEquals(t, actual, board) + +} + +func TestGetScreenboards(t *testing.T) { + boards, err := client.GetScreenboards() + if err != nil { + t.Fatalf("Retrieving screenboards failed when it shouldn't: %s", err) + } + num := len(boards) + + board := createTestScreenboard(t) + defer cleanUpScreenboard(t, board.Id) + + boards, err = client.GetScreenboards() + if err != nil { + t.Fatalf("Retrieving screenboards failed when it shouldn't: %s", err) + } + + if num+1 != len(boards) { + t.Fatalf("Number of screenboards didn't match expected: %d != %d", len(boards), num+1) + } +} + +func getTestScreenboard() *datadog.Screenboard { + return &datadog.Screenboard{ + Title: "___Test-Board___", + Height: "600", + Width: "800", + Widgets: []datadog.Widget{}, + } +} + +func createTestScreenboard(t *testing.T) *datadog.Screenboard { + board := getTestScreenboard() + board, err := client.CreateScreenboard(board) + if err != nil { + t.Fatalf("Creating a screenboard failed when it shouldn't: %s", err) + } + + return board +} + +func cleanUpScreenboard(t *testing.T, id int) { + if err := client.DeleteScreenboard(id); err != nil { + t.Fatalf("Deleting a screenboard failed when it shouldn't. Manual cleanup needed. (%s)", err) + } + + deletedBoard, err := client.GetScreenboard(id) + if deletedBoard != nil { + t.Fatal("Screenboard hasn't been deleted when it should have been. Manual cleanup needed.") + } + + if err == nil { + t.Fatal("Fetching deleted screenboard didn't lead to an error. Manual cleanup needed.") + } +} + +func assertScreenboardEquals(t *testing.T, actual, expected *datadog.Screenboard) { + if actual.Title != expected.Title { + t.Errorf("Screenboard title does not match: %s != %s", actual.Title, expected.Title) + } + if actual.Width != expected.Width { + t.Errorf("Screenboard width does not match: %s != %s", actual.Width, expected.Width) + } + if actual.Height != expected.Height { + t.Errorf("Screenboard width does not match: %s != %s", actual.Height, expected.Height) + } + if len(actual.Widgets) != len(expected.Widgets) { + t.Errorf("Number of Screenboard widgets does not match: %d != %d", len(actual.Widgets), len(expected.Widgets)) + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/integration/test_helpers.go b/vendor/github.com/zorkian/go-datadog-api/integration/test_helpers.go new file mode 100644 index 000000000..dba24dbed --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/integration/test_helpers.go @@ -0,0 +1,24 @@ +package integration + +import ( + "github.com/zorkian/go-datadog-api" + "log" + "os" +) + +var ( + apiKey string + appKey string + client *datadog.Client +) + +func initTest() *datadog.Client { + apiKey = os.Getenv("DATADOG_API_KEY") + appKey = os.Getenv("DATADOG_APP_KEY") + + if apiKey == "" || appKey == "" { + log.Fatal("Please make sure to set the env variables 'DATADOG_API_KEY' and 'DATADOG_APP_KEY' before running this test") + } + + return datadog.NewClient(apiKey, appKey) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/main.go b/vendor/github.com/zorkian/go-datadog-api/main.go new file mode 100644 index 000000000..10ec467d4 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/main.go @@ -0,0 +1,30 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import "net/http" + +// Client is the object that handles talking to the Datadog API. This maintains +// state information for a particular application connection. +type Client struct { + apiKey, appKey string + + //The Http Client that is used to make requests + HttpClient *http.Client +} + +// NewClient returns a new datadog.Client which can be used to access the API +// methods. The expected argument is the API key. +func NewClient(apiKey, appKey string) *Client { + return &Client{ + apiKey: apiKey, + appKey: appKey, + HttpClient: http.DefaultClient, + } +} diff --git a/vendor/github.com/zorkian/go-datadog-api/monitors.go b/vendor/github.com/zorkian/go-datadog-api/monitors.go new file mode 100644 index 000000000..a9cae660f --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/monitors.go @@ -0,0 +1,113 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "encoding/json" + "fmt" +) + +type ThresholdCount struct { + Ok json.Number `json:"ok,omitempty"` + Critical json.Number `json:"critical,omitempty"` + Warning json.Number `json:"warning,omitempty"` +} + +type Options struct { + NoDataTimeframe int `json:"no_data_timeframe,omitempty"` + NotifyAudit bool `json:"notify_audit,omitempty"` + NotifyNoData bool `json:"notify_no_data,omitempty"` + RenotifyInterval int `json:"renotify_interval,omitempty"` + Silenced map[string]int `json:"silenced,omitempty"` + TimeoutH int `json:"timeout_h,omitempty"` + EscalationMessage string `json:"escalation_message,omitempty"` + Thresholds ThresholdCount `json:"thresholds,omitempty"` + IncludeTags bool `json:"include_tags,omitempty"` +} + +//Monitors allow you to watch a metric or check that you care about, +//notifying your team when some defined threshold is exceeded. +type Monitor struct { + Id int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Query string `json:"query,omitempty"` + Name string `json:"name,omitempty"` + Message string `json:"message,omitempty"` + Tags []string `json:"tags,omitempty"` + Options Options `json:"options,omitempty"` +} + +// reqMonitors receives a slice of all monitors +type reqMonitors struct { + Monitors []Monitor `json:"monitors,omitempty"` +} + +// Createmonitor adds a new monitor to the system. This returns a pointer to an +// monitor so you can pass that to Updatemonitor later if needed. +func (self *Client) CreateMonitor(monitor *Monitor) (*Monitor, error) { + var out Monitor + err := self.doJsonRequest("POST", "/v1/monitor", monitor, &out) + if err != nil { + return nil, err + } + return &out, nil +} + +// Updatemonitor takes an monitor that was previously retrieved through some method +// and sends it back to the server. +func (self *Client) UpdateMonitor(monitor *Monitor) error { + return self.doJsonRequest("PUT", fmt.Sprintf("/v1/monitor/%d", monitor.Id), + monitor, nil) +} + +// Getmonitor retrieves an monitor by identifier. +func (self *Client) GetMonitor(id int) (*Monitor, error) { + var out Monitor + err := self.doJsonRequest("GET", fmt.Sprintf("/v1/monitor/%d", id), nil, &out) + if err != nil { + return nil, err + } + return &out, nil +} + +// Deletemonitor removes an monitor from the system. +func (self *Client) DeleteMonitor(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/monitor/%d", id), + nil, nil) +} + +// GetMonitors returns a slice of all monitors. +func (self *Client) GetMonitors() ([]Monitor, error) { + var out reqMonitors + err := self.doJsonRequest("GET", "/v1/monitor", nil, &out.Monitors) + if err != nil { + return nil, err + } + return out.Monitors, nil +} + +// MuteMonitors turns off monitoring notifications. +func (self *Client) MuteMonitors() error { + return self.doJsonRequest("POST", "/v1/monitor/mute_all", nil, nil) +} + +// UnmuteMonitors turns on monitoring notifications. +func (self *Client) UnmuteMonitors() error { + return self.doJsonRequest("POST", "/v1/monitor/unmute_all", nil, nil) +} + +// MuteMonitor turns off monitoring notifications for a monitor. +func (self *Client) MuteMonitor(id int) error { + return self.doJsonRequest("POST", fmt.Sprintf("/v1/monitor/%d/mute", id), nil, nil) +} + +// UnmuteMonitor turns on monitoring notifications for a monitor. +func (self *Client) UnmuteMonitor(id int) error { + return self.doJsonRequest("POST", fmt.Sprintf("/v1/monitor/%d/unmute", id), nil, nil) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/request.go b/vendor/github.com/zorkian/go-datadog-api/request.go new file mode 100644 index 000000000..d161914f6 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/request.go @@ -0,0 +1,131 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + "time" + + "github.com/cenkalti/backoff" +) + +// uriForAPI is to be called with something like "/v1/events" and it will give +// the proper request URI to be posted to. +func (self *Client) uriForAPI(api string) string { + url := os.Getenv("DATADOG_HOST") + if url == "" { + url = "https://app.datadoghq.com" + } + if strings.Index(api, "?") > -1 { + return url + "/api" + api + "&api_key=" + + self.apiKey + "&application_key=" + self.appKey + } else { + return url + "/api" + api + "?api_key=" + + self.apiKey + "&application_key=" + self.appKey + } +} + +// doJsonRequest is the simplest type of request: a method on a URI that returns +// some JSON result which we unmarshal into the passed interface. +func (self *Client) doJsonRequest(method, api string, + reqbody, out interface{}) error { + // Handle the body if they gave us one. + var bodyreader io.Reader + if method != "GET" && reqbody != nil { + bjson, err := json.Marshal(reqbody) + if err != nil { + return err + } + bodyreader = bytes.NewReader(bjson) + } + + req, err := http.NewRequest(method, self.uriForAPI(api), bodyreader) + if err != nil { + return err + } + if bodyreader != nil { + req.Header.Add("Content-Type", "application/json") + } + + // Perform the request and retry it if it's not a POST request + var resp *http.Response + if method == "POST" { + resp, err = self.HttpClient.Do(req) + } else { + resp, err = self.doRequestWithRetries(req, 60*time.Second) + } + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("API error %s: %s", resp.Status, body) + } + + // If they don't care about the body, then we don't care to give them one, + // so bail out because we're done. + if out == nil { + return nil + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // If we got no body, by default let's just make an empty JSON dict. This + // saves us some work in other parts of the code. + if len(body) == 0 { + body = []byte{'{', '}'} + } + + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + return nil +} + +// doRequestWithRetries performs an HTTP request repeatedly for maxTime or until +// no error and no HTTP response code higher than 299 is returned. +func (self *Client) doRequestWithRetries(req *http.Request, maxTime time.Duration) (*http.Response, error) { + var ( + err error + resp *http.Response + bo = backoff.NewExponentialBackOff() + ) + bo.MaxElapsedTime = maxTime + + err = backoff.Retry(func() error { + resp, err = self.HttpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return errors.New("API error: " + resp.Status) + } + return nil + }, bo) + + return resp, err +} diff --git a/vendor/github.com/zorkian/go-datadog-api/screen_widgets.go b/vendor/github.com/zorkian/go-datadog-api/screen_widgets.go new file mode 100644 index 000000000..69e159abe --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/screen_widgets.go @@ -0,0 +1,287 @@ +package datadog + +type TextSize struct { + Size int + Auto bool +} + +type TileDef struct { + Events []TileDefEvent `json:"events,omitempty"` + Markers []TimeseriesMarker `json:"markers,omitempty"` + Requests []TimeseriesRequest `json:"requests,omitempty"` + Viz string `json:"viz,omitempty"` +} + +type TimeseriesRequest struct { + Query string `json:"q,omitempty"` + Type string `json:"type,omitempty"` + ConditionalFormats []ConditionalFormat `json:"conditional_formats,omitempty"` + Style TimeseriesRequestStyle `json:"style,omitempty"` +} + +type TimeseriesRequestStyle struct { + Palette string `json:"palette,omitempty"` +} + +type TimeseriesMarker struct { + Label string `json:"label,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` +} + +type TileDefEvent struct { + Query string `json:"q"` +} + +type AlertValueWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TextAlign string `json:"text_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Precision int `json:"precision,omitempty"` + AlertId int `json:"alert_id,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + AddTimeframe bool `json:"add_timeframe,omitempty"` + Y int `json:"y,omitempty"` + X int `json:"x,omitempty"` + TextSize string `json:"text_size,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + Type string `json:"type,omitempty"` + Unit string `json:"unit,omitempty"` +} + +type ChangeWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + Aggregator string `json:"aggregator,omitempty"` + TileDef TileDef `json:"tile_def,omitempty"` +} + +type GraphWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + Type string `json:"type,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + LegendSize int `json:"legend_size,omitempty"` + Legend bool `json:"legend,omitempty"` + TileDef TileDef `json:"tile_def,omitempty"` +} + +type EventTimelineWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + Type string `json:"type,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + Query string `json:"query,omitempty"` +} + +type AlertGraphWidget struct { + TitleSize int `json:"title_size,omitempty"` + VizType string `json:"timeseries,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + AlertId int `json:"alert_id,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + Type string `json:"type,omitempty"` + AddTimeframe bool `json:"add_timeframe,omitempty"` +} + +type HostMapWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + Query string `json:"query,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + LegendSize int `json:"legend_size,omitempty"` + Type string `json:"type,omitempty"` + Legend bool `json:"legend,omitempty"` + TileDef TileDef `json:"tile_def,omitempty"` +} + +type CheckStatusWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TextAlign string `json:"text_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + Tags string `json:"tags,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + TextSize string `json:"text_size,omitempty"` + Type string `json:"type,omitempty"` + Check string `json:"check,omitempty"` + Group string `json:"group,omitempty"` + Grouping string `json:"grouping,omitempty"` +} + +type IFrameWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + Url string `json:"url,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"y,omitempty"` + Y int `json:"x,omitempty"` + Type string `json:"type,omitempty"` +} + +type NoteWidget struct { + TitleSize int `json:"title_size,omitempty"` + Title bool `json:"title,omitempty"` + RefreshEvery int `json:"refresh_every,omitempty"` + TickPos string `json:"tick_pos,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TickEdge string `json:"tick_edge,omitempty"` + TextAlign string `json:"text_align,omitempty"` + TitleText string `json:"title_text,omitempty"` + Height int `json:"height,omitempty"` + Color string `json:"bgcolor,omitempty"` + Html string `json:"html,omitempty"` + Y int `json:"y,omitempty"` + X int `json:"x,omitempty"` + FontSize int `json:"font_size,omitempty"` + Tick bool `json:"tick,omitempty"` + Note string `json:"type,omitempty"` + Width int `json:"width,omitempty"` + AutoRefresh bool `json:"auto_refresh,omitempty"` +} + +type TimeseriesWidget struct { + Height int `json:"height,omitempty"` + Legend bool `json:"legend,omitempty"` + TileDef TileDef `json:"tile_def,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleSize TextSize `json:"title_size,omitempty"` + TitleText string `json:"title_text,omitempty"` + Type string `json:"type,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} + +type QueryValueWidget struct { + Timeframe string `json:"timeframe,omitempty"` + TimeframeAggregator string `json:"aggr,omitempty"` + Aggregator string `json:"aggregator,omitempty"` + CalcFunc string `json:"calc_func,omitempty"` + ConditionalFormats []ConditionalFormat `json:"conditional_formats,omitempty"` + Height int `json:"height,omitempty"` + IsValidQuery bool `json:"is_valid_query,omitempty,omitempty"` + Metric string `json:"metric,omitempty"` + MetricType string `json:"metric_type,omitempty"` + Precision int `json:"precision,omitempty"` + Query string `json:"query,omitempty"` + ResultCalcFunc string `json:"res_calc_func,omitempty"` + Tags []string `json:"tags,omitempty"` + TextAlign string `json:"text_align,omitempty"` + TextSize TextSize `json:"text_size,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleSize TextSize `json:"title_size,omitempty"` + TitleText string `json:"title_text,omitempty"` + Type string `json:"type,omitempty"` + Unit string `json:"auto,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} +type ConditionalFormat struct { + Color string `json:"color,omitempty"` + Comparator string `json:"comparator,omitempty"` + Inverted bool `json:"invert,omitempty"` + Value int `json:"value,omitempty"` +} + +type ToplistWidget struct { + Height int `json:"height,omitempty"` + Legend bool `json:"legend,omitempty"` + LegendSize int `json:"legend_size,omitempty"` + TileDef TileDef `json:"tile_def,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleSize TextSize `json:"title_size,omitempty"` + TitleText string `json:"title_text,omitempty"` + Type string `json:"type,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} + +type EventStreamWidget struct { + EventSize string `json:"event_size,omitempty"` + Height int `json:"height,omitempty"` + Query string `json:"query,omitempty"` + Timeframe string `json:"timeframe,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleSize TextSize `json:"title_size,omitempty"` + TitleText string `json:"title_text,omitempty"` + Type string `json:"type,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} + +type FreeTextWidget struct { + Color string `json:"color,omitempty"` + FontSize string `json:"font_size,omitempty"` + Height int `json:"height,omitempty"` + Text string `json:"text,omitempty"` + TextAlign string `json:"text_align,omitempty"` + Type string `json:"type,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} + +type ImageWidget struct { + Height int `json:"height,omitempty"` + Sizing string `json:"sizing,omitempty"` + Title bool `json:"title,omitempty"` + TitleAlign string `json:"title_align,omitempty"` + TitleSize TextSize `json:"title_size,omitempty"` + TitleText string `json:"title_text,omitempty"` + Type string `json:"type,omitempty"` + Url string `json:"url,omitempty"` + Width int `json:"width,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` +} diff --git a/vendor/github.com/zorkian/go-datadog-api/screenboards.go b/vendor/github.com/zorkian/go-datadog-api/screenboards.go new file mode 100644 index 000000000..2317e392b --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/screenboards.go @@ -0,0 +1,117 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import ( + "fmt" +) + +// Screenboard represents a user created screenboard. This is the full screenboard +// struct when we load a screenboard in detail. +type Screenboard struct { + Id int `json:"id,omitempty"` + Title string `json:"board_title,omitempty"` + Height string `json:"height,omitempty"` + Width string `json:"width,omitempty"` + Shared bool `json:"shared,omitempty"` + Templated bool `json:"templated,omitempty"` + TemplateVariables []TemplateVariable `json:"template_variables,omitempty"` + Widgets []Widget `json:"widgets,omitempty"` +} + +//type Widget struct { +type Widget struct { + Default string `json:"default,omitempty"` + Name string `json:"name,omitempty"` + Prefix string `json:"prefix,omitempty"` + TimeseriesWidget TimeseriesWidget `json:"timeseries,omitempty"` + QueryValueWidget QueryValueWidget `json:"query_value,omitempty"` + EventStreamWidget EventStreamWidget `json:"event_stream,omitempty"` + FreeTextWidget FreeTextWidget `json:"free_text,omitempty"` + ToplistWidget ToplistWidget `json:"toplist,omitempty"` + ImageWidget ImageWidget `json:"image,omitempty"` + ChangeWidget ChangeWidget `json:"change,omitempty"` + GraphWidget GraphWidget `json:"graph,omitempty"` + EventTimelineWidget EventTimelineWidget `json:"event_timeline,omitempty"` + AlertValueWidget AlertValueWidget `json:"alert_value,omitempty"` + AlertGraphWidget AlertGraphWidget `json:"alert_graph,omitempty"` + HostMapWidget HostMapWidget `json:"hostmap,omitempty"` + CheckStatusWidget CheckStatusWidget `json:"check_status,omitempty"` + IFrameWidget IFrameWidget `json:"iframe,omitempty"` + NoteWidget NoteWidget `json:"frame,omitempty"` +} + +// ScreenboardLite represents a user created screenboard. This is the mini +// struct when we load the summaries. +type ScreenboardLite struct { + Id int `json:"id,omitempty"` + Resource string `json:"resource,omitempty"` + Title string `json:"title,omitempty"` +} + +// reqGetScreenboards from /api/v1/screen +type reqGetScreenboards struct { + Screenboards []*ScreenboardLite `json:"screenboards"` +} + +// GetScreenboard returns a single screenboard created on this account. +func (self *Client) GetScreenboard(id int) (*Screenboard, error) { + out := &Screenboard{} + err := self.doJsonRequest("GET", fmt.Sprintf("/v1/screen/%d", id), nil, out) + if err != nil { + return nil, err + } + return out, nil +} + +// GetScreenboards returns a list of all screenboards created on this account. +func (self *Client) GetScreenboards() ([]*ScreenboardLite, error) { + var out reqGetScreenboards + err := self.doJsonRequest("GET", "/v1/screen", nil, &out) + if err != nil { + return nil, err + } + return out.Screenboards, nil +} + +// DeleteScreenboard deletes a screenboard by the identifier. +func (self *Client) DeleteScreenboard(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/screen/%d", id), nil, nil) +} + +// CreateScreenboard creates a new screenboard when given a Screenboard struct. Note +// that the Id, Resource, Url and similar elements are not used in creation. +func (self *Client) CreateScreenboard(board *Screenboard) (*Screenboard, error) { + out := &Screenboard{} + if err := self.doJsonRequest("POST", "/v1/screen", board, out); err != nil { + return nil, err + } + return out, nil +} + +// UpdateScreenboard in essence takes a Screenboard struct and persists it back to +// the server. Use this if you've updated your local and need to push it back. +func (self *Client) UpdateScreenboard(board *Screenboard) error { + return self.doJsonRequest("PUT", fmt.Sprintf("/v1/screen/%d", board.Id), board, nil) +} + +type ScreenShareResponse struct { + BoardId int `json:"board_id"` + PublicUrl string `json:"public_url"` +} + +// ShareScreenboard shares an existing screenboard, it takes and updates ScreenShareResponse +func (self *Client) ShareScreenboard(id int, response *ScreenShareResponse) error { + return self.doJsonRequest("GET", fmt.Sprintf("/v1/screen/share/%d", id), nil, response) +} + +// RevokeScreenboard revokes a currently shared screenboard +func (self *Client) RevokeScreenboard(id int) error { + return self.doJsonRequest("DELETE", fmt.Sprintf("/v1/screen/share/%d", id), nil, nil) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/search.go b/vendor/github.com/zorkian/go-datadog-api/search.go new file mode 100644 index 000000000..b5e83efde --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/search.go @@ -0,0 +1,37 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +// reqSearch is the container for receiving search results. +type reqSearch struct { + Results struct { + Hosts []string `json:"hosts,omitempty"` + Metrics []string `json:"metrics,omitempty"` + } `json:"results"` +} + +// SearchHosts searches through the hosts facet, returning matching hostnames. +func (self *Client) SearchHosts(search string) ([]string, error) { + var out reqSearch + err := self.doJsonRequest("GET", "/v1/search?q=hosts:"+search, nil, &out) + if err != nil { + return nil, err + } + return out.Results.Hosts, nil +} + +// SearchMetrics searches through the metrics facet, returning matching ones. +func (self *Client) SearchMetrics(search string) ([]string, error) { + var out reqSearch + err := self.doJsonRequest("GET", "/v1/search?q=metrics:"+search, nil, &out) + if err != nil { + return nil, err + } + return out.Results.Metrics, nil +} diff --git a/vendor/github.com/zorkian/go-datadog-api/series.go b/vendor/github.com/zorkian/go-datadog-api/series.go new file mode 100644 index 000000000..1bfe0efd1 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/series.go @@ -0,0 +1,68 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +import "strconv" + +// DataPoint is a tuple of [UNIX timestamp, value]. This has to use floats +// because the value could be non-integer. +type DataPoint [2]float64 + +// Metric represents a collection of data points that we might send or receive +// on one single metric line. +type Metric struct { + Metric string `json:"metric,omitempty"` + Points []DataPoint `json:"points,omitempty"` + Type string `json:"type,omitempty"` + Host string `json:"host,omitempty"` + Tags []string `json:"tags,omitempty"` +} + +// Series represents a collection of data points we get when we query for timeseries data +type Series struct { + Metric string `json:"metric,omitempty"` + DisplayName string `json:"display_name,omitempty"` + Points []DataPoint `json:"pointlist,omitempty"` + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + Interval int `json:"interval,omitempty"` + Aggr string `json:"aggr,omitempty"` + Length int `json:"length,omitempty"` + Scope string `json:"scope,omitempty"` + Expression string `json:"expression,omitempty"` +} + +// reqPostSeries from /api/v1/series +type reqPostSeries struct { + Series []Metric `json:"series,omitempty"` +} + +// reqMetrics is the container for receiving metric results. +type reqMetrics struct { + Series []Series `json:"series,omitempty"` +} + +// PostMetrics takes as input a slice of metrics and then posts them up to the +// server for posting data. +func (client *Client) PostMetrics(series []Metric) error { + return client.doJsonRequest("POST", "/v1/series", + reqPostSeries{Series: series}, nil) +} + +// QueryMetrics takes as input from, to (seconds from Unix Epoch) and query string and then requests +// timeseries data for that time peried +func (client *Client) QueryMetrics(from, to int64, query string) ([]Series, error) { + var out reqMetrics + err := client.doJsonRequest("GET", "/v1/query?from="+strconv.FormatInt(from, 10)+"&to="+strconv.FormatInt(to, 10)+"&query="+query, + nil, &out) + if err != nil { + return nil, err + } + return out.Series, nil +} diff --git a/vendor/github.com/zorkian/go-datadog-api/snapshot.go b/vendor/github.com/zorkian/go-datadog-api/snapshot.go new file mode 100644 index 000000000..145a19bcc --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/snapshot.go @@ -0,0 +1,33 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2016 by authors and contributors. + */ + +package datadog + +import ( + "fmt" + "net/url" + "time" +) + +// Snapshot creates an image from a graph and returns the URL of the image. +func (self *Client) Snapshot(query string, start, end time.Time, eventQuery string) (string, error) { + v := url.Values{} + v.Add("start", fmt.Sprintf("%d", start.Unix())) + v.Add("end", fmt.Sprintf("%d", end.Unix())) + v.Add("metric_query", query) + v.Add("event_query", eventQuery) + + out := struct { + SnapshotURL string `json:"snapshot_url"` + }{} + err := self.doJsonRequest("GET", "/v1/graph/snapshot?"+v.Encode(), nil, &out) + if err != nil { + return "", err + } + return out.SnapshotURL, nil +} diff --git a/vendor/github.com/zorkian/go-datadog-api/tags.go b/vendor/github.com/zorkian/go-datadog-api/tags.go new file mode 100644 index 000000000..20043d1ae --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/tags.go @@ -0,0 +1,96 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +// TagMap is used to receive the format given to us by the API. +type TagMap map[string][]string + +// reqGetTags is the container for receiving tags. +type reqGetTags struct { + Tags TagMap `json:"tags,omitempty"` +} + +// regGetHostTags is for receiving a slice of tags. +type reqGetHostTags struct { + Tags []string `json:"tags,omitempty"` +} + +// GetTags returns a map of tags. +func (self *Client) GetTags(source string) (TagMap, error) { + var out reqGetTags + uri := "/v1/tags/hosts" + if source != "" { + uri += "?source=" + source + } + err := self.doJsonRequest("GET", uri, nil, &out) + if err != nil { + return nil, err + } + return out.Tags, nil +} + +// GetHostTags returns a slice of tags for a given host and source. +func (self *Client) GetHostTags(host, source string) ([]string, error) { + var out reqGetHostTags + uri := "/v1/tags/hosts/" + host + if source != "" { + uri += "?source=" + source + } + err := self.doJsonRequest("GET", uri, nil, &out) + if err != nil { + return nil, err + } + return out.Tags, nil +} + +// GetHostTagsBySource is a different way of viewing the tags. It returns a map +// of source:[tag,tag]. +func (self *Client) GetHostTagsBySource(host, source string) (TagMap, error) { + var out reqGetTags + uri := "/v1/tags/hosts/" + host + "?by_source=true" + if source != "" { + uri += "&source=" + source + } + err := self.doJsonRequest("GET", uri, nil, &out) + if err != nil { + return nil, err + } + return out.Tags, nil +} + +// AddTagsToHost does exactly what it says on the tin. Given a list of tags, +// add them to the host. The source is optionally specificed, and defaults to +// "users" as per the API documentation. +func (self *Client) AddTagsToHost(host, source string, tags []string) error { + uri := "/v1/tags/hosts/" + host + if source != "" { + uri += "?source=" + source + } + return self.doJsonRequest("POST", uri, reqGetHostTags{Tags: tags}, nil) +} + +// UpdateHostTags overwrites existing tags for a host, allowing you to specify +// a new set of tags for the given source. This defaults to "users". +func (self *Client) UpdateHostTags(host, source string, tags []string) error { + uri := "/v1/tags/hosts/" + host + if source != "" { + uri += "?source=" + source + } + return self.doJsonRequest("PUT", uri, reqGetHostTags{Tags: tags}, nil) +} + +// RemoveHostTags removes all tags from a host for the given source. If none is +// given, the API defaults to "users". +func (self *Client) RemoveHostTags(host, source string) error { + uri := "/v1/tags/hosts/" + host + if source != "" { + uri += "?source=" + source + } + return self.doJsonRequest("DELETE", uri, nil, nil) +} diff --git a/vendor/github.com/zorkian/go-datadog-api/users.go b/vendor/github.com/zorkian/go-datadog-api/users.go new file mode 100644 index 000000000..76b4da573 --- /dev/null +++ b/vendor/github.com/zorkian/go-datadog-api/users.go @@ -0,0 +1,71 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2013 by authors and contributors. + */ + +package datadog + +type User struct { + Handle string `json:"handle,omitempty"` + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` + Role string `json:"role,omitempty"` + IsAdmin bool `json:"is_admin,omitempty"` + Verified bool `json:"verified,omitempty"` + Disabled bool `json:"disabled,omitempty"` +} + +// reqInviteUsers contains email addresses to send invitations to. +type reqInviteUsers struct { + Emails []string `json:"emails,omitempty"` +} + +// InviteUsers takes a slice of email addresses and sends invitations to them. +func (self *Client) InviteUsers(emails []string) error { + return self.doJsonRequest("POST", "/v1/invite_users", + reqInviteUsers{Emails: emails}, nil) +} + +// internal type to retrieve users from the api +type usersData struct { + Users []User `json:"users"` +} + +// GetUsers returns all user, or an error if not found +func (self *Client) GetUsers() (users []User, err error) { + var udata usersData + uri := "/v1/user" + err = self.doJsonRequest("GET", uri, nil, &udata) + users = udata.Users + return +} + +// internal type to retrieve single user from the api +type userData struct { + User User `json:"user"` +} + +// GetUser returns the user that match a handle, or an error if not found +func (self *Client) GetUser(handle string) (user User, err error) { + var udata userData + uri := "/v1/user/" + handle + err = self.doJsonRequest("GET", uri, nil, &udata) + user = udata.User + return +} + +// UpdateUser updates a user with the content of `user`, +// and returns an error if the update failed +func (self *Client) UpdateUser(user User) error { + uri := "/v1/user/" + user.Handle + return self.doJsonRequest("PUT", uri, user, nil) +} + +// DeleteUser deletes a user and returns an error if deletion failed +func (self *Client) DeleteUser(handle string) error { + uri := "/v1/user/" + handle + return self.doJsonRequest("DELETE", uri, nil, nil) +} From c8bd02abee1246a76e3ab5e921ee5ae54f4f5b82 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Mon, 8 Feb 2016 20:00:00 +1100 Subject: [PATCH 6/6] Add Datadog doco. --- website/source/assets/stylesheets/_docs.scss | 1 + .../providers/datadog/index.html.markdown | 38 ++++++++ .../providers/datadog/r/monitor.html.markdown | 94 +++++++++++++++++++ website/source/layouts/datadog.erb | 26 +++++ website/source/layouts/docs.erb | 4 + 5 files changed, 163 insertions(+) create mode 100644 website/source/docs/providers/datadog/index.html.markdown create mode 100644 website/source/docs/providers/datadog/r/monitor.html.markdown create mode 100644 website/source/layouts/datadog.erb diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 9737e0f53..c1a6e629d 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -14,6 +14,7 @@ body.layout-azurerm, body.layout-cloudflare, body.layout-cloudstack, body.layout-consul, +body.layout-datadog, body.layout-digitalocean, body.layout-dme, body.layout-dnsimple, diff --git a/website/source/docs/providers/datadog/index.html.markdown b/website/source/docs/providers/datadog/index.html.markdown new file mode 100644 index 000000000..9e84050fe --- /dev/null +++ b/website/source/docs/providers/datadog/index.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "datadog" +page_title: "Provider: Datadog" +sidebar_current: "docs-datadog-index" +description: |- + The Datadog provider is used to interact with the resources supported by Datadog. The provider needs to be configured with the proper credentials before it can be used. +--- + +# Datadog Provider + +The [Datadog](https://www.datadoghq.com) provider is used to interact with the +resources supported by Datadog. The provider needs to be configured +with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Datadog provider +provider "datadog" { + api_key = "${var.datadog_api_key}" + app_key = "${var.datadog_app_key}" +} + +# Create a new monitor +resource "datadog_monitor" "default" { + ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `api_key` - (Required) Datadog API key +* `app_key` - (Required) Datadog APP key + diff --git a/website/source/docs/providers/datadog/r/monitor.html.markdown b/website/source/docs/providers/datadog/r/monitor.html.markdown new file mode 100644 index 000000000..e0032888c --- /dev/null +++ b/website/source/docs/providers/datadog/r/monitor.html.markdown @@ -0,0 +1,94 @@ +--- +layout: "datadog" +page_title: "Datadog: datadog_monitor" +sidebar_current: "docs-datadog-resource-monitor" +description: |- + Provides a Datadog monitor resource. This can be used to create and manage monitors. +--- + +# datadog\_monitor + +Provides a Datadog monitor resource. This can be used to create and manage Datadog monitors. + +## Example Usage + +``` +# Create a new Datadog monitor +resource "datadog_monitor" "foo" { + name = "Name for monitor foo" + type = "Metric alert" + message = "Monitor triggered. Notify: @hipchat-channel" + escalation_message = "Escalation message @pagerduty" + + query = "avg(last_1h):avg:aws.ec2.cpu{environment:foo,host:foo} by {host} > 2" + + thresholds { + ok = 0 + warning = 1 + critical = 2 + } + + notify_no_data = false + renotify_interval = 60 + + notify_audit = false + timeout_h = 60 + include_tags = true + silenced { + "*" = 0 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Required) The type of the monitor, chosen from: + * `metric alert` + * `service check` + * `event alert` + * `query alert` +* `name` - (Required) Name of Datadog monitor +* `query` - (Required) The monitor query to notify on with syntax varying depending on what type of monitor + you are creating. See [API Reference](http://docs.datadoghq.com/api) for options. +* `message` - (Required) A message to include with notifications for this monitor. + Email notifications can be sent to specific users by using the same '@username' notation as events. +* `escalation_message` - (Optional) A message to include with a re-notification. Supports the '@username' + notification allowed elsewhere. +* `thresholds` - (Required) Thresholds by threshold type: + * `ok` + * `warning` + * `critical` +* `notify_no_data` (Optional) A boolean indicating whether this monitor will notify when data stops reporting. Defaults + to false. +* `no_data_timeframe` (Optional) The number of minutes before a monitor will notify when data stops reporting. Must be at + least 2x the monitor timeframe for metric alerts or 2 minutes for service checks. Default: 2x timeframe for + metric alerts, 2 minutes for service checks. +* `renotify_interval` (Optional) The number of minutes after the last notification before a monitor will re-notify + on the current status. It will only re-notify if it's not resolved. +* `notify_audit` (Optional) A boolean indicating whether tagged users will be notified on changes to this monitor. + Defaults to false. +* `timeout_h` (Optional) The number of hours of the monitor not reporting data before it will automatically resolve + from a triggered state. Defaults to false. +* `include_tags` (Optional) A boolean indicating whether notifications from this monitor will automatically insert its + triggering tags into the title. Defaults to true. +* `silenced` (Optional) Each scope will be muted until the given POSIX timestamp or forever if the value is 0. + + To mute the alert completely: + + silenced { + '*' = 0 + } + + To mute role:db for a short time: + + silenced { + 'role:db' = 1412798116 + } + +## Attributes Reference + +The following attributes are exported: + +* `id` - ID of the Datadog monitor diff --git a/website/source/layouts/datadog.erb b/website/source/layouts/datadog.erb new file mode 100644 index 000000000..77f816429 --- /dev/null +++ b/website/source/layouts/datadog.erb @@ -0,0 +1,26 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %> diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 53b810b19..a000ec6c8 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -158,6 +158,10 @@ Consul + > + Datadog + + > DigitalOcean