diff --git a/api_keys_test.go b/api_keys_test.go index 8027737..1a96ea5 100644 --- a/api_keys_test.go +++ b/api_keys_test.go @@ -9,11 +9,10 @@ package datadog_test import ( + "encoding/json" "testing" "time" - "encoding/json" - "github.com/stretchr/testify/assert" dd "github.com/zorkian/go-datadog-api" ) diff --git a/datadog-accessors.go b/datadog-accessors.go index 2e5fc7e..c8beac7 100644 --- a/datadog-accessors.go +++ b/datadog-accessors.go @@ -13654,6 +13654,626 @@ func (s *ServiceHookSlackRequest) SetUrl(v string) { s.Url = &v } +// GetCreatedAt returns the CreatedAt field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetCreatedAt() int { + if s == nil || s.CreatedAt == nil { + return 0 + } + return *s.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetCreatedAtOk() (int, bool) { + if s == nil || s.CreatedAt == nil { + return 0, false + } + return *s.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasCreatedAt() bool { + if s != nil && s.CreatedAt != nil { + return true + } + + return false +} + +// SetCreatedAt allocates a new s.CreatedAt and returns the pointer to it. +func (s *ServiceLevelObjective) SetCreatedAt(v int) { + s.CreatedAt = &v +} + +// GetCreator returns the Creator field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetCreator() Creator { + if s == nil || s.Creator == nil { + return Creator{} + } + return *s.Creator +} + +// GetCreatorOk returns a tuple with the Creator field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetCreatorOk() (Creator, bool) { + if s == nil || s.Creator == nil { + return Creator{}, false + } + return *s.Creator, true +} + +// HasCreator returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasCreator() bool { + if s != nil && s.Creator != nil { + return true + } + + return false +} + +// SetCreator allocates a new s.Creator and returns the pointer to it. +func (s *ServiceLevelObjective) SetCreator(v Creator) { + s.Creator = &v +} + +// GetDescription returns the Description field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetDescription() string { + if s == nil || s.Description == nil { + return "" + } + return *s.Description +} + +// GetDescriptionOk returns a tuple with the Description field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetDescriptionOk() (string, bool) { + if s == nil || s.Description == nil { + return "", false + } + return *s.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasDescription() bool { + if s != nil && s.Description != nil { + return true + } + + return false +} + +// SetDescription allocates a new s.Description and returns the pointer to it. +func (s *ServiceLevelObjective) SetDescription(v string) { + s.Description = &v +} + +// GetID returns the ID field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetID() string { + if s == nil || s.ID == nil { + return "" + } + return *s.ID +} + +// GetIDOk returns a tuple with the ID field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetIDOk() (string, bool) { + if s == nil || s.ID == nil { + return "", false + } + return *s.ID, true +} + +// HasID returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasID() bool { + if s != nil && s.ID != nil { + return true + } + + return false +} + +// SetID allocates a new s.ID and returns the pointer to it. +func (s *ServiceLevelObjective) SetID(v string) { + s.ID = &v +} + +// GetModifiedAt returns the ModifiedAt field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetModifiedAt() int { + if s == nil || s.ModifiedAt == nil { + return 0 + } + return *s.ModifiedAt +} + +// GetModifiedAtOk returns a tuple with the ModifiedAt field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetModifiedAtOk() (int, bool) { + if s == nil || s.ModifiedAt == nil { + return 0, false + } + return *s.ModifiedAt, true +} + +// HasModifiedAt returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasModifiedAt() bool { + if s != nil && s.ModifiedAt != nil { + return true + } + + return false +} + +// SetModifiedAt allocates a new s.ModifiedAt and returns the pointer to it. +func (s *ServiceLevelObjective) SetModifiedAt(v int) { + s.ModifiedAt = &v +} + +// GetMonitorSearch returns the MonitorSearch field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetMonitorSearch() string { + if s == nil || s.MonitorSearch == nil { + return "" + } + return *s.MonitorSearch +} + +// GetMonitorSearchOk returns a tuple with the MonitorSearch field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetMonitorSearchOk() (string, bool) { + if s == nil || s.MonitorSearch == nil { + return "", false + } + return *s.MonitorSearch, true +} + +// HasMonitorSearch returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasMonitorSearch() bool { + if s != nil && s.MonitorSearch != nil { + return true + } + + return false +} + +// SetMonitorSearch allocates a new s.MonitorSearch and returns the pointer to it. +func (s *ServiceLevelObjective) SetMonitorSearch(v string) { + s.MonitorSearch = &v +} + +// GetName returns the Name field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetName() string { + if s == nil || s.Name == nil { + return "" + } + return *s.Name +} + +// GetNameOk returns a tuple with the Name field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetNameOk() (string, bool) { + if s == nil || s.Name == nil { + return "", false + } + return *s.Name, true +} + +// HasName returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasName() bool { + if s != nil && s.Name != nil { + return true + } + + return false +} + +// SetName allocates a new s.Name and returns the pointer to it. +func (s *ServiceLevelObjective) SetName(v string) { + s.Name = &v +} + +// GetQuery returns the Query field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetQuery() ServiceLevelObjectiveMetricQuery { + if s == nil || s.Query == nil { + return ServiceLevelObjectiveMetricQuery{} + } + return *s.Query +} + +// GetQueryOk returns a tuple with the Query field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetQueryOk() (ServiceLevelObjectiveMetricQuery, bool) { + if s == nil || s.Query == nil { + return ServiceLevelObjectiveMetricQuery{}, false + } + return *s.Query, true +} + +// HasQuery returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasQuery() bool { + if s != nil && s.Query != nil { + return true + } + + return false +} + +// SetQuery allocates a new s.Query and returns the pointer to it. +func (s *ServiceLevelObjective) SetQuery(v ServiceLevelObjectiveMetricQuery) { + s.Query = &v +} + +// GetType returns the Type field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetType() string { + if s == nil || s.Type == nil { + return "" + } + return *s.Type +} + +// GetTypeOk returns a tuple with the Type field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetTypeOk() (string, bool) { + if s == nil || s.Type == nil { + return "", false + } + return *s.Type, true +} + +// HasType returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasType() bool { + if s != nil && s.Type != nil { + return true + } + + return false +} + +// SetType allocates a new s.Type and returns the pointer to it. +func (s *ServiceLevelObjective) SetType(v string) { + s.Type = &v +} + +// GetTypeID returns the TypeID field if non-nil, zero value otherwise. +func (s *ServiceLevelObjective) GetTypeID() int { + if s == nil || s.TypeID == nil { + return 0 + } + return *s.TypeID +} + +// GetTypeIDOk returns a tuple with the TypeID field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjective) GetTypeIDOk() (int, bool) { + if s == nil || s.TypeID == nil { + return 0, false + } + return *s.TypeID, true +} + +// HasTypeID returns a boolean if a field has been set. +func (s *ServiceLevelObjective) HasTypeID() bool { + if s != nil && s.TypeID != nil { + return true + } + + return false +} + +// SetTypeID allocates a new s.TypeID and returns the pointer to it. +func (s *ServiceLevelObjective) SetTypeID(v int) { + s.TypeID = &v +} + +// GetID returns the ID field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) GetID() string { + if s == nil || s.ID == nil { + return "" + } + return *s.ID +} + +// GetIDOk returns a tuple with the ID field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) GetIDOk() (string, bool) { + if s == nil || s.ID == nil { + return "", false + } + return *s.ID, true +} + +// HasID returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) HasID() bool { + if s != nil && s.ID != nil { + return true + } + + return false +} + +// SetID allocates a new s.ID and returns the pointer to it. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) SetID(v string) { + s.ID = &v +} + +// GetMessage returns the Message field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) GetMessage() string { + if s == nil || s.Message == nil { + return "" + } + return *s.Message +} + +// GetMessageOk returns a tuple with the Message field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) GetMessageOk() (string, bool) { + if s == nil || s.Message == nil { + return "", false + } + return *s.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) HasMessage() bool { + if s != nil && s.Message != nil { + return true + } + + return false +} + +// SetMessage allocates a new s.Message and returns the pointer to it. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) SetMessage(v string) { + s.Message = &v +} + +// GetTimeFrame returns the TimeFrame field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) GetTimeFrame() string { + if s == nil || s.TimeFrame == nil { + return "" + } + return *s.TimeFrame +} + +// GetTimeFrameOk returns a tuple with the TimeFrame field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) GetTimeFrameOk() (string, bool) { + if s == nil || s.TimeFrame == nil { + return "", false + } + return *s.TimeFrame, true +} + +// HasTimeFrame returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) HasTimeFrame() bool { + if s != nil && s.TimeFrame != nil { + return true + } + + return false +} + +// SetTimeFrame allocates a new s.TimeFrame and returns the pointer to it. +func (s *ServiceLevelObjectiveDeleteTimeFramesError) SetTimeFrame(v string) { + s.TimeFrame = &v +} + +// GetDenominator returns the Denominator field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveMetricQuery) GetDenominator() string { + if s == nil || s.Denominator == nil { + return "" + } + return *s.Denominator +} + +// GetDenominatorOk returns a tuple with the Denominator field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveMetricQuery) GetDenominatorOk() (string, bool) { + if s == nil || s.Denominator == nil { + return "", false + } + return *s.Denominator, true +} + +// HasDenominator returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveMetricQuery) HasDenominator() bool { + if s != nil && s.Denominator != nil { + return true + } + + return false +} + +// SetDenominator allocates a new s.Denominator and returns the pointer to it. +func (s *ServiceLevelObjectiveMetricQuery) SetDenominator(v string) { + s.Denominator = &v +} + +// GetNumerator returns the Numerator field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveMetricQuery) GetNumerator() string { + if s == nil || s.Numerator == nil { + return "" + } + return *s.Numerator +} + +// GetNumeratorOk returns a tuple with the Numerator field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveMetricQuery) GetNumeratorOk() (string, bool) { + if s == nil || s.Numerator == nil { + return "", false + } + return *s.Numerator, true +} + +// HasNumerator returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveMetricQuery) HasNumerator() bool { + if s != nil && s.Numerator != nil { + return true + } + + return false +} + +// SetNumerator allocates a new s.Numerator and returns the pointer to it. +func (s *ServiceLevelObjectiveMetricQuery) SetNumerator(v string) { + s.Numerator = &v +} + +// GetSLO returns the SLO field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveThreshold) GetSLO() float64 { + if s == nil || s.SLO == nil { + return 0 + } + return *s.SLO +} + +// GetSLOOk returns a tuple with the SLO field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveThreshold) GetSLOOk() (float64, bool) { + if s == nil || s.SLO == nil { + return 0, false + } + return *s.SLO, true +} + +// HasSLO returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveThreshold) HasSLO() bool { + if s != nil && s.SLO != nil { + return true + } + + return false +} + +// SetSLO allocates a new s.SLO and returns the pointer to it. +func (s *ServiceLevelObjectiveThreshold) SetSLO(v float64) { + s.SLO = &v +} + +// GetSLODisplay returns the SLODisplay field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveThreshold) GetSLODisplay() string { + if s == nil || s.SLODisplay == nil { + return "" + } + return *s.SLODisplay +} + +// GetSLODisplayOk returns a tuple with the SLODisplay field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveThreshold) GetSLODisplayOk() (string, bool) { + if s == nil || s.SLODisplay == nil { + return "", false + } + return *s.SLODisplay, true +} + +// HasSLODisplay returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveThreshold) HasSLODisplay() bool { + if s != nil && s.SLODisplay != nil { + return true + } + + return false +} + +// SetSLODisplay allocates a new s.SLODisplay and returns the pointer to it. +func (s *ServiceLevelObjectiveThreshold) SetSLODisplay(v string) { + s.SLODisplay = &v +} + +// GetTimeFrame returns the TimeFrame field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveThreshold) GetTimeFrame() string { + if s == nil || s.TimeFrame == nil { + return "" + } + return *s.TimeFrame +} + +// GetTimeFrameOk returns a tuple with the TimeFrame field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveThreshold) GetTimeFrameOk() (string, bool) { + if s == nil || s.TimeFrame == nil { + return "", false + } + return *s.TimeFrame, true +} + +// HasTimeFrame returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveThreshold) HasTimeFrame() bool { + if s != nil && s.TimeFrame != nil { + return true + } + + return false +} + +// SetTimeFrame allocates a new s.TimeFrame and returns the pointer to it. +func (s *ServiceLevelObjectiveThreshold) SetTimeFrame(v string) { + s.TimeFrame = &v +} + +// GetWarning returns the Warning field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveThreshold) GetWarning() float64 { + if s == nil || s.Warning == nil { + return 0 + } + return *s.Warning +} + +// GetWarningOk returns a tuple with the Warning field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveThreshold) GetWarningOk() (float64, bool) { + if s == nil || s.Warning == nil { + return 0, false + } + return *s.Warning, true +} + +// HasWarning returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveThreshold) HasWarning() bool { + if s != nil && s.Warning != nil { + return true + } + + return false +} + +// SetWarning allocates a new s.Warning and returns the pointer to it. +func (s *ServiceLevelObjectiveThreshold) SetWarning(v float64) { + s.Warning = &v +} + +// GetWarningDisplay returns the WarningDisplay field if non-nil, zero value otherwise. +func (s *ServiceLevelObjectiveThreshold) GetWarningDisplay() string { + if s == nil || s.WarningDisplay == nil { + return "" + } + return *s.WarningDisplay +} + +// GetWarningDisplayOk returns a tuple with the WarningDisplay field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (s *ServiceLevelObjectiveThreshold) GetWarningDisplayOk() (string, bool) { + if s == nil || s.WarningDisplay == nil { + return "", false + } + return *s.WarningDisplay, true +} + +// HasWarningDisplay returns a boolean if a field has been set. +func (s *ServiceLevelObjectiveThreshold) HasWarningDisplay() bool { + if s != nil && s.WarningDisplay != nil { + return true + } + + return false +} + +// SetWarningDisplay allocates a new s.WarningDisplay and returns the pointer to it. +func (s *ServiceLevelObjectiveThreshold) SetWarningDisplay(v string) { + s.WarningDisplay = &v +} + // GetServiceKey returns the ServiceKey field if non-nil, zero value otherwise. func (s *servicePD) GetServiceKey() string { if s == nil || s.ServiceKey == nil { @@ -16723,6 +17343,37 @@ func (t *Time) SetLiveSpan(v string) { t.LiveSpan = &v } +// GetData returns the Data field if non-nil, zero value otherwise. +func (t *timeframesDeleteResp) GetData() ServiceLevelObjectiveDeleteTimeFramesResponse { + if t == nil || t.Data == nil { + return ServiceLevelObjectiveDeleteTimeFramesResponse{} + } + return *t.Data +} + +// GetDataOk returns a tuple with the Data field if it's non-nil, zero value otherwise +// and a boolean to check if the value has been set. +func (t *timeframesDeleteResp) GetDataOk() (ServiceLevelObjectiveDeleteTimeFramesResponse, bool) { + if t == nil || t.Data == nil { + return ServiceLevelObjectiveDeleteTimeFramesResponse{}, false + } + return *t.Data, true +} + +// HasData returns a boolean if a field has been set. +func (t *timeframesDeleteResp) HasData() bool { + if t != nil && t.Data != nil { + return true + } + + return false +} + +// SetData allocates a new t.Data and returns the pointer to it. +func (t *timeframesDeleteResp) SetData(v ServiceLevelObjectiveDeleteTimeFramesResponse) { + t.Data = &v +} + // GetFrom returns the From field if non-nil, zero value otherwise. func (t *TimeRange) GetFrom() json.Number { if t == nil || t.From == nil { diff --git a/helper_test.go b/helper_test.go index 4856721..f92bfc6 100644 --- a/helper_test.go +++ b/helper_test.go @@ -9,9 +9,9 @@ package datadog_test import ( + "encoding/json" "testing" - "encoding/json" "github.com/stretchr/testify/assert" "github.com/zorkian/go-datadog-api" ) diff --git a/helpers.go b/helpers.go index 3d6f53c..6e4b79d 100644 --- a/helpers.go +++ b/helpers.go @@ -11,6 +11,7 @@ package datadog import ( "encoding/json" "errors" + "math" "strconv" ) @@ -42,6 +43,25 @@ func GetIntOk(v *int) (int, bool) { return 0, false } +// Float64 is a helper routine that allocates a new float64 value +// to store v and returns a pointer to it. +func Float64(v float64) *float64 { return &v } + +// GetFloat64Ok is a helper routine that returns a boolean representing +// if a value was set, and if so, dereferences the pointer to it. +func GetFloat64Ok(v *float64) (float64, bool) { + if v != nil { + return *v, true + } + + return 0, false +} + +// Float64AlmostEqual will return true if two floats are within a certain tolerance of each other +func Float64AlmostEqual(a, b, tolerance float64) bool { + return math.Abs(a-b) < tolerance +} + // String is a helper routine that allocates a new string value // to store v and returns a pointer to it. func String(v string) *string { return &v } diff --git a/integration/service_level_objectives_test.go b/integration/service_level_objectives_test.go new file mode 100644 index 0000000..5bd036f --- /dev/null +++ b/integration/service_level_objectives_test.go @@ -0,0 +1,140 @@ +package integration + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zorkian/go-datadog-api" +) + +func TestServiceLevelObjectivesCreateGetUpdateAndDelete(t *testing.T) { + expected := &datadog.ServiceLevelObjective{ + Name: datadog.String("Integration Test SLO - 'Test Create, Update and Delete'"), + Description: datadog.String("Integration test for SLOs"), + Tags: []string{"test:integration"}, + Thresholds: datadog.ServiceLevelObjectiveThresholds{ + { + TimeFrame: datadog.String("7d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + }, + Type: &datadog.ServiceLevelObjectiveTypeMetric, + Query: &datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String("sum:my.metric{type:good}.as_count()"), + Denominator: datadog.String("sum:my.metric{*}.as_count()"), + }, + } + + // Create + actual, err := client.CreateServiceLevelObjective(expected) + assert.NoError(t, err) + assert.NotEmpty(t, actual.GetID()) + assert.Equal(t, expected.Name, actual.Name) + assert.Equal(t, expected.Description, actual.Description) + assert.True(t, expected.Thresholds.Equal(actual.Thresholds)) + + // Get + found, err := client.GetServiceLevelObjective(actual.GetID()) + assert.NoError(t, err) + assert.Equal(t, actual.GetID(), found.GetID()) + + // Update + actual.SetDescription("Integration test for SLOs - updated") + actual.Thresholds = datadog.ServiceLevelObjectiveThresholds{ + { + TimeFrame: datadog.String("7d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + { + TimeFrame: datadog.String("30d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + } + actual, err = client.UpdateServiceLevelObjective(actual) + assert.NoError(t, err) + assert.Equal(t, "Integration test for SLOs - updated", actual.GetDescription()) + assert.Len(t, actual.Thresholds, 2) + + // Delete + err = client.DeleteServiceLevelObjective(actual.GetID()) + assert.NoError(t, err) +} + +func TestServiceLevelObjectivesBulkTimeFrameDelete(t *testing.T) { + expected1 := &datadog.ServiceLevelObjective{ + Name: datadog.String("Integration Test SLO - 'Test Multi Time Frame Delete 1'"), + Description: datadog.String("Integration test for SLOs"), + Tags: []string{"test:integration"}, + Thresholds: datadog.ServiceLevelObjectiveThresholds{ + { + TimeFrame: datadog.String("7d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + { + TimeFrame: datadog.String("30d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + { + TimeFrame: datadog.String("90d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + }, + Type: &datadog.ServiceLevelObjectiveTypeMetric, + Query: &datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String("sum:my.metric{type:good}.as_count()"), + Denominator: datadog.String("sum:my.metric{*}.as_count()"), + }, + } + expected2 := &datadog.ServiceLevelObjective{ + Name: datadog.String("Integration Test SLO - 'Test Multi Time Frame Delete 2'"), + Description: datadog.String("Integration test for SLOs"), + Tags: []string{"test:integration"}, + Thresholds: datadog.ServiceLevelObjectiveThresholds{ + { + TimeFrame: datadog.String("7d"), + SLO: datadog.Float64(99), + Warning: datadog.Float64(99.5), + }, + }, + Type: &datadog.ServiceLevelObjectiveTypeMetric, + Query: &datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String("sum:my.metric{type:good}.as_count()"), + Denominator: datadog.String("sum:my.metric{*}.as_count()"), + }, + } + + // Create + actual1, err := client.CreateServiceLevelObjective(expected1) + assert.NoError(t, err) + assert.NotEmpty(t, actual1.GetID()) + actual2, err := client.CreateServiceLevelObjective(expected2) + assert.NoError(t, err) + assert.NotEmpty(t, actual2.GetID()) + + // Do multi-timeframe delete + timeframesToDelete := map[string][]string{ + // delete only 2 of 3 timeframes from 1 + actual1.GetID(): { + "30d", "90d", + }, + // delete all timeframes from 2 + actual2.GetID(): { + "7d", + }, + } + + resp, err := client.DeleteServiceLevelObjectiveTimeFrames(timeframesToDelete) + assert.EqualValues(t, []string{actual2.GetID()}, resp.DeletedIDs) + assert.EqualValues(t, []string{actual1.GetID()}, resp.UpdatedIDs) + + // Delete + err = client.DeleteServiceLevelObjective(actual1.GetID()) + assert.NoError(t, err) +} diff --git a/service_level_objectives.go b/service_level_objectives.go new file mode 100644 index 0000000..fa0bbdd --- /dev/null +++ b/service_level_objectives.go @@ -0,0 +1,469 @@ +/* + * Datadog API for Go + * + * Please see the included LICENSE file for licensing information. + * + * Copyright 2017 by authors and contributors. + */ + +package datadog + +import ( + "encoding/json" + "fmt" + "net/url" + "regexp" + "strings" + "time" +) + +// Define the available machine-readable SLO types +const ( + ServiceLevelObjectiveTypeMonitorID int = 0 + ServiceLevelObjectiveTypeMetricID int = 1 +) + +// Define the available human-readable SLO types +var ( + ServiceLevelObjectiveTypeMonitor = "monitor" + ServiceLevelObjectiveTypeMetric = "metric" +) + +// ServiceLevelObjectiveTypeFromID maps machine-readable type to human-readable type +var ServiceLevelObjectiveTypeFromID = map[int]string{ + ServiceLevelObjectiveTypeMonitorID: ServiceLevelObjectiveTypeMonitor, + ServiceLevelObjectiveTypeMetricID: ServiceLevelObjectiveTypeMetric, +} + +// ServiceLevelObjectiveTypeToID maps human-readable type to machine-readable type +var ServiceLevelObjectiveTypeToID = map[string]int{ + ServiceLevelObjectiveTypeMonitor: ServiceLevelObjectiveTypeMonitorID, + ServiceLevelObjectiveTypeMetric: ServiceLevelObjectiveTypeMetricID, +} + +// ServiceLevelObjectiveThreshold defines an SLO threshold and timeframe +// For example it's the ` of within +type ServiceLevelObjectiveThreshold struct { + TimeFrame *string `json:"timeframe,omitempty"` + SLO *float64 `json:"slo,omitempty"` + SLODisplay *string `json:"slo_display,omitempty"` // Read-Only for monitor type + Warning *float64 `json:"warning,omitempty"` + WarningDisplay *string `json:"warning_display,omitempty"` // Read-Only for monitor type +} + +const thresholdTolerance float64 = 1e-8 + +// Equal check if one threshold is equal to another. +func (s *ServiceLevelObjectiveThreshold) Equal(o interface{}) bool { + other, ok := o.(*ServiceLevelObjectiveThreshold) + if !ok { + return false + } + + return s.GetTimeFrame() == other.GetTimeFrame() && + Float64AlmostEqual(s.GetSLO(), other.GetSLO(), thresholdTolerance) && + Float64AlmostEqual(s.GetWarning(), other.GetWarning(), thresholdTolerance) +} + +// String implements Stringer +func (s ServiceLevelObjectiveThreshold) String() string { + return fmt.Sprintf("Threshold{timeframe=%s slo=%f slo_display=%s warning=%f warning_display=%s", + s.GetTimeFrame(), s.GetSLO(), s.GetSLODisplay(), s.GetWarning(), s.GetWarningDisplay()) +} + +// ServiceLevelObjectiveMetricQuery represents a metric-based SLO definition query +// Numerator is the sum of the `good` events +// Denominator is the sum of the `total` events +type ServiceLevelObjectiveMetricQuery struct { + Numerator *string `json:"numerator,omitempty"` + Denominator *string `json:"denominator,omitempty"` +} + +// ServiceLevelObjectiveThresholds is a sortable array of ServiceLevelObjectiveThreshold(s) +type ServiceLevelObjectiveThresholds []*ServiceLevelObjectiveThreshold + +// Len implements sort.Interface length +func (s ServiceLevelObjectiveThresholds) Len() int { + return len(s) +} + +// Less implements sort.Interface less comparator +func (s ServiceLevelObjectiveThresholds) Less(i, j int) bool { + iDur, _ := ServiceLevelObjectiveTimeFrameToDuration(s[i].GetTimeFrame()) + jDur, _ := ServiceLevelObjectiveTimeFrameToDuration(s[j].GetTimeFrame()) + return iDur < jDur +} + +// Swap implements sort.Interface swap method +func (s ServiceLevelObjectiveThresholds) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Equal check if one set of thresholds is equal to another. +func (s ServiceLevelObjectiveThresholds) Equal(o interface{}) bool { + other, ok := o.(ServiceLevelObjectiveThresholds) + if !ok { + return false + } + + if len(s) != len(other) { + // easy case + return false + } + + // compare one set from another + sSet := make(map[string]*ServiceLevelObjectiveThreshold, 0) + for _, t := range s { + sSet[t.GetTimeFrame()] = t + } + oSet := make(map[string]*ServiceLevelObjectiveThreshold, 0) + for _, t := range other { + oSet[t.GetTimeFrame()] = t + } + + for timeframe, t := range oSet { + threshold, ok := sSet[timeframe] + if !ok { + // other contains more + return false + } + + if !threshold.Equal(t) { + // they differ + return false + } + // drop from sSet for efficiency + delete(sSet, timeframe) + } + // if there are any remaining then they differ + if len(sSet) > 0 { + return false + } + return true +} + +// ServiceLevelObjective defines the Service Level Objective entity +type ServiceLevelObjective struct { + // Common + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Thresholds ServiceLevelObjectiveThresholds `json:"thresholds,omitempty"` + Type *string `json:"type,omitempty"` + TypeID *int `json:"type_id,omitempty"` // Read-Only + // SLI definition + Query *ServiceLevelObjectiveMetricQuery `json:"query,omitempty"` + MonitorIDs []int `json:"monitor_ids,omitempty"` + MonitorSearch *string `json:"monitor_search,omitempty"` + Groups []string `json:"groups,omitempty"` + + // Informational + MonitorTags []string `json:"monitor_tags,omitempty"` // Read-Only + Creator *Creator `json:"creator,omitempty"` // Read-Only + CreatedAt *int `json:"created_at"` // Read-Only + ModifiedAt *int `json:"modified_at"` // Read-Only +} + +var sloTimeFrameToDurationRegex = regexp.MustCompile(`(?P\d+)(?P(d))`) + +// ServiceLevelObjectiveTimeFrameToDuration will convert a timeframe into a duration +func ServiceLevelObjectiveTimeFrameToDuration(timeframe string) (time.Duration, error) { + match := sloTimeFrameToDurationRegex.FindStringSubmatch(timeframe) + result := make(map[string]string) + for i, name := range sloTimeFrameToDurationRegex.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + if len(result) != 2 { + return 0, fmt.Errorf("invalid timeframe specified: '%s'", timeframe) + } + qty, err := json.Number(result["quantity"]).Int64() + if err != nil { + return 0, fmt.Errorf("invalid timeframe specified, could not convert quantity to number") + } + if qty <= 0 { + return 0, fmt.Errorf("invalid timeframe specified, quantity must be a positive number") + } + + switch result["unit"] { + // FUTURE: will support more time frames, hence the switch here. + default: + // only matches on `d` currently, so this is simple + return time.Hour * 24 * time.Duration(qty), nil + } +} + +type createSLOThreshold struct { + SLO *float64 `json:"slo,omitempty"` + Warning *float64 `json:"warning,omitempty"` +} + +// createServiceLevelObjective is the appropriate model for creation/update +// note that thresholds is a map of timeframe: +type createUpdateServiceLevelObjective struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Tags []string `json:"tags,omitempty"` + Thresholds map[string]*createSLOThreshold `json:"thresholds,omitempty"` + Type *string `json:"type,omitempty"` + Query *ServiceLevelObjectiveMetricQuery `json:"query,omitempty"` + MonitorIDs []int `json:"monitor_ids,omitempty"` + MonitorSearch *string `json:"monitor_search,omitempty"` + Groups []string `json:"groups,omitempty"` +} + +func (slo *ServiceLevelObjective) toCreateUpdate() *createUpdateServiceLevelObjective { + thresholds := make(map[string]*createSLOThreshold, 0) + for _, threshold := range slo.Thresholds { + thresholds[threshold.GetTimeFrame()] = &createSLOThreshold{ + SLO: threshold.SLO, + Warning: threshold.Warning, + } + } + + return &createUpdateServiceLevelObjective{ + Name: slo.Name, + Description: slo.Description, + Tags: slo.Tags, + Thresholds: thresholds, + Type: slo.Type, + Query: slo.Query, + MonitorIDs: slo.MonitorIDs, + MonitorSearch: slo.MonitorSearch, + Groups: slo.Groups, + } +} + +// CreateServiceLevelObjective adds a new service level objective to the system. This returns a pointer +// to the service level objective so you can pass that to UpdateServiceLevelObjective or DeleteServiceLevelObjective +// later if needed. +func (client *Client) CreateServiceLevelObjective(slo *ServiceLevelObjective) (*ServiceLevelObjective, error) { + var out reqServiceLevelObjectives + + if slo == nil { + return nil, fmt.Errorf("no SLO specified") + } + + if err := client.doJsonRequest("POST", "/v1/slo", slo.toCreateUpdate(), &out); err != nil { + return nil, err + } + if out.Error != "" { + return nil, fmt.Errorf(out.Error) + } + + return out.Data[0], nil +} + +// UpdateServiceLevelObjective takes a service level objective that was previously retrieved through some method +// and sends it back to the server. +func (client *Client) UpdateServiceLevelObjective(slo *ServiceLevelObjective) (*ServiceLevelObjective, error) { + var out reqServiceLevelObjectives + + if slo == nil { + return nil, fmt.Errorf("no SLO specified") + } + + if err := client.doJsonRequest("PUT", fmt.Sprintf("/v1/slo/%s", slo.GetID()), slo.toCreateUpdate(), &out); err != nil { + return nil, err + } + + if out.Error != "" { + return nil, fmt.Errorf(out.Error) + } + + return out.Data[0], nil + +} + +type reqServiceLevelObjectives struct { + Data []*ServiceLevelObjective `json:"data"` + Error string `json:"error"` +} + +// SearchServiceLevelObjectives searches for service level objectives by search criteria. +// limit will limit the amount of SLO's returned, the API will enforce a maximum and default to a minimum if not specified +func (client *Client) SearchServiceLevelObjectives(limit int, offset int, query string, ids []string) ([]*ServiceLevelObjective, error) { + var out reqServiceLevelObjectives + uriValues := make(url.Values, 0) + if limit > 0 { + uriValues.Set("limit", fmt.Sprintf("%d", limit)) + } + if offset >= 0 { + uriValues.Set("offset", fmt.Sprintf("%d", offset)) + } + // Either use `query` or use `ids` + hasQuery := query != "" + hasIDs := len(ids) > 0 + if hasQuery && hasIDs { + return nil, fmt.Errorf("invalid search: must specify either ids OR query, not both") + } + + // specify by query + if hasQuery { + uriValues.Set("query", query) + } + // specify by `ids` + if hasIDs { + uriValues.Set("ids", strings.Join(ids, ",")) + } + + uri := "/v1/slo" + encodedQuery := uriValues.Encode() + if encodedQuery != "" { + uri += "?" + encodedQuery + } + + if err := client.doJsonRequest("GET", uri, nil, &out); err != nil { + return nil, err + } + + if out.Error != "" { + return nil, fmt.Errorf(out.Error) + } + + return out.Data, nil +} + +type reqSingleServiceLevelObjective struct { + Data *ServiceLevelObjective `json:"data"` + Error string `json:"error"` +} + +// GetServiceLevelObjective retrieves an service level objective by identifier. +func (client *Client) GetServiceLevelObjective(id string) (*ServiceLevelObjective, error) { + var out reqSingleServiceLevelObjective + + if id == "" { + return nil, fmt.Errorf("no SLO specified") + } + + if err := client.doJsonRequest("GET", fmt.Sprintf("/v1/slo/%s", id), nil, &out); err != nil { + return nil, err + } + if out.Error != "" { + return nil, fmt.Errorf(out.Error) + } + + return out.Data, nil +} + +// GetServiceLevelObjectives retrieves an service level objective by identifier. +func (client *Client) GetServiceLevelObjectives(ids []string) ([]*ServiceLevelObjective, error) { + var out reqServiceLevelObjectives + + if len(ids) == 0 { + return nil, fmt.Errorf("no SLO IDs specified") + } + + if err := client.doJsonRequest("GET", "/v1/slo", ids, &out); err != nil { + return nil, err + } + if out.Error != "" { + return nil, fmt.Errorf(out.Error) + } + + return out.Data, nil +} + +type reqDeleteResp struct { + Data []string `json:"data"` + Error string `json:"error"` +} + +// DeleteServiceLevelObjective removes an service level objective from the system. +func (client *Client) DeleteServiceLevelObjective(id string) error { + var out reqDeleteResp + + if id == "" { + return fmt.Errorf("no SLO specified") + } + + if err := client.doJsonRequest("DELETE", fmt.Sprintf("/v1/slo/%s", id), nil, &out); err != nil { + return err + } + + if out.Error != "" { + return fmt.Errorf(out.Error) + } + + return nil +} + +// DeleteServiceLevelObjectives removes multiple service level objective from the system by id. +func (client *Client) DeleteServiceLevelObjectives(ids []string) error { + var out reqDeleteResp + + if len(ids) == 0 { + return fmt.Errorf("no SLOs specified") + } + + if err := client.doJsonRequest("DELETE", "/v1/slo", ids, &out); err != nil { + return err + } + + if out.Error != "" { + return fmt.Errorf(out.Error) + } + + return nil +} + +// ServiceLevelObjectiveDeleteTimeFramesResponse is the response unique to the delete individual time-frames request +// this is read-only +type ServiceLevelObjectiveDeleteTimeFramesResponse struct { + DeletedIDs []string `json:"deleted"` + UpdatedIDs []string `json:"updated"` +} + +// ServiceLevelObjectiveDeleteTimeFramesError is the error specific to deleting individual time frames. +// It contains more detailed information than the standard error. +type ServiceLevelObjectiveDeleteTimeFramesError struct { + ID *string `json:"id"` + TimeFrame *string `json:"timeframe"` + Message *string `json:"message"` +} + +// Error computes the human readable error +func (e ServiceLevelObjectiveDeleteTimeFramesError) Error() string { + return fmt.Sprintf("error=%s id=%s for timeframe=%s", e.GetMessage(), e.GetID(), e.GetTimeFrame()) +} + +type timeframesDeleteResp struct { + Data *ServiceLevelObjectiveDeleteTimeFramesResponse `json:"data"` + Errors []*ServiceLevelObjectiveDeleteTimeFramesError `json:"errors"` +} + +// DeleteServiceLevelObjectiveTimeFrames will delete SLO timeframes individually. +// This is useful if you have a SLO with 3 time windows and only need to delete some of the time windows. +// It will do a full delete if all time windows are removed as a result. +// +// Example: +// SLO `12345678901234567890123456789012` was defined with 2 time frames: "7d" and "30d" +// SLO `abcdefabcdefabcdefabcdefabcdefab` was defined with 2 time frames: "30d" and "90d" +// +// When we delete `7d` from `12345678901234567890123456789012` we still have `30d` timeframe remaining, hence this is "updated" +// When we delete `30d` and `90d` from `abcdefabcdefabcdefabcdefabcdefab` we are left with 0 time frames, hence this is "deleted" +// and the entire SLO config is deleted +func (client *Client) DeleteServiceLevelObjectiveTimeFrames(timeframeByID map[string][]string) (*ServiceLevelObjectiveDeleteTimeFramesResponse, error) { + var out timeframesDeleteResp + + if len(timeframeByID) == 0 { + return nil, fmt.Errorf("nothing specified") + } + + if err := client.doJsonRequest("POST", "/v1/slo/bulk_delete", &timeframeByID, &out); err != nil { + return nil, err + } + + if out.Errors != nil && len(out.Errors) > 0 { + errMsgs := make([]string, 0) + for _, e := range out.Errors { + errMsgs = append(errMsgs, e.Error()) + } + return nil, fmt.Errorf("errors deleting timeframes: %s", strings.Join(errMsgs, ",")) + } + + return out.Data, nil +} diff --git a/service_level_objectives_test.go b/service_level_objectives_test.go new file mode 100644 index 0000000..baf2fd6 --- /dev/null +++ b/service_level_objectives_test.go @@ -0,0 +1,256 @@ +package datadog + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func sptr(i string) *string { + return &i +} + +func TestServiceLevelObjectiveSerialization(t *testing.T) { + slo := ServiceLevelObjective{ + ID: sptr("12345678901234567890123456789012"), + Name: sptr("Test SLO"), + Description: sptr("Test Description"), + Tags: []string{"product:foo"}, + Thresholds: []*ServiceLevelObjectiveThreshold{ + { + TimeFrame: String("7d"), + SLO: Float64(99), + Warning: Float64(99.5), + }, + }, + Type: &ServiceLevelObjectiveTypeMonitor, + MonitorIDs: []int{1}, + } + + raw, err := json.Marshal(&slo) + assert.NoError(t, err) + assert.NotEmpty(t, raw) + + var deserializedSLO ServiceLevelObjective + + err = json.Unmarshal(raw, &deserializedSLO) + assert.NoError(t, err) + assert.Equal(t, slo.ID, deserializedSLO.ID) + assert.Equal(t, slo.Name, deserializedSLO.Name) + assert.Equal(t, slo.Description, deserializedSLO.Description) + assert.EqualValues(t, slo.Tags, deserializedSLO.Tags) + assert.EqualValues(t, slo.Thresholds, deserializedSLO.Thresholds) + assert.Equal(t, slo.Type, deserializedSLO.Type) + assert.EqualValues(t, slo.MonitorIDs, deserializedSLO.MonitorIDs) + assert.Nil(t, deserializedSLO.Groups) +} + +func testSLOGetMock(t *testing.T, fixturePath string) (*httptest.Server, *Client) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response, err := ioutil.ReadFile(fixturePath) + if err != nil { + t.Fatal(err) + } + w.Write(response) + })) + + datadogClient := Client{ + baseUrl: ts.URL, + HttpClient: http.DefaultClient, + } + return ts, &datadogClient +} + +func getMockSLO(id string) *ServiceLevelObjective { + return &ServiceLevelObjective{ + ID: &id, + Name: sptr("Test SLO"), + Description: sptr("Test Description"), + Tags: []string{"product:foo"}, + Thresholds: []*ServiceLevelObjectiveThreshold{ + { + TimeFrame: String("7d"), + SLO: Float64(99), + Warning: Float64(99.5), + }, + { + TimeFrame: String("30d"), + SLO: Float64(98), + Warning: Float64(99), + }, + { + TimeFrame: String("90d"), + SLO: Float64(98), + Warning: Float64(99), + }, + }, + Type: &ServiceLevelObjectiveTypeMonitor, + MonitorIDs: []int{1}, + } +} + +func TestServiceLevelObjectiveIntegration(t *testing.T) { + + t.Run("CreateMonitor", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/create_response.json") + defer ts.Close() + + slo := getMockSLO("") + created, err := c.CreateServiceLevelObjective(slo) + assert.NoError(t2, err) + assert.Equal(t2, "12345678901234567890123456789012", created.GetID()) + }) + + t.Run("CreateMetric", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/create_response_metric.json") + defer ts.Close() + + slo := getMockSLO("") + created, err := c.CreateServiceLevelObjective(slo) + assert.NoError(t2, err) + assert.Equal(t2, "abcdefabcdefabcdefabcdefabcdefab", created.GetID()) + }) + + t.Run("Update", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/update_response.json") + defer ts.Close() + + slo := getMockSLO("12345678901234567890123456789012") + slo, err := c.UpdateServiceLevelObjective(slo) + assert.NoError(t2, err) + assert.Equal(t2, "12345678901234567890123456789012", slo.GetID()) + assert.Equal(t2, 1563283900, slo.GetModifiedAt()) + }) + + t.Run("Delete", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/delete_response.json") + defer ts.Close() + + slo := getMockSLO("12345678901234567890123456789012") + err := c.DeleteServiceLevelObjective(slo.GetID()) + assert.NoError(t2, err) + }) + + t.Run("DeleteMany", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/delete_many_response.json") + defer ts.Close() + + err := c.DeleteServiceLevelObjectives( + []string{"12345678901234567890123456789012", "abcdefabcdefabcdefabcdefabcdefab"}, + ) + assert.NoError(t2, err) + }) + + t.Run("DeleteByTimeframe", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/delete_by_timeframe_response.json") + defer ts.Close() + + /* Some Context for this test case: This is useful for doing individual time-frame deletes across different SLOs (used by the web list view bulk delete) + + `12345678901234567890123456789012` was defined with 2 time frames: "7d" and "30d" + `abcdefabcdefabcdefabcdefabcdefab` was defined with 2 time frames: "30d" and "90d" + + When we delete `7d` from `12345678901234567890123456789012` we still have `30d` timeframe remaining, hence this is "updated" + When we delete `30d` and `90d` from `abcdefabcdefabcdefabcdefabcdefab` we are left with 0 time frames, hence this is "deleted" + and the entire SLO config is deleted + */ + resp, err := c.DeleteServiceLevelObjectiveTimeFrames(map[string][]string{ + "12345678901234567890123456789012": {"7d"}, + "abcdefabcdefabcdefabcdefabcdefab": {"30d", "90d"}, + }) + assert.NoError(t2, err) + assert.EqualValues(t2, resp.UpdatedIDs, []string{"12345678901234567890123456789012"}) + assert.EqualValues(t2, resp.DeletedIDs, []string{"abcdefabcdefabcdefabcdefabcdefab"}) + }) + + t.Run("GetByID", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/get_by_id_response.json") + defer ts.Close() + + slo, err := c.GetServiceLevelObjective("12345678901234567890123456789012") + assert.NoError(t2, err) + assert.Equal(t2, "12345678901234567890123456789012", slo.GetID()) + }) + + t.Run("GetManyWithIDs", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/get_many_response.json") + defer ts.Close() + + slos, err := c.GetServiceLevelObjectives([]string{"12345678901234567890123456789012", "abcdefabcdefabcdefabcdefabcdefab"}) + assert.NoError(t2, err) + assert.Len(t2, slos, 2) + + contains := func(slos []*ServiceLevelObjective, id string) bool { + for _, slo := range slos { + if slo.GetID() == id { + return true + } + } + return false + } + assert.True(t2, contains(slos, "12345678901234567890123456789012")) + assert.True(t2, contains(slos, "abcdefabcdefabcdefabcdefabcdefab")) + }) + + t.Run("Search", func(t2 *testing.T) { + ts, c := testSLOGetMock(t2, "./tests/fixtures/service_level_objectives/search_response.json") + defer ts.Close() + + slos, err := c.SearchServiceLevelObjectives(1000, 0, "service:foo AND team:a", nil) + assert.NoError(t2, err) + assert.Len(t2, slos, 1) + assert.Equal(t2, "12345678901234567890123456789012", slos[0].GetID()) + }) + + t.Run("thresholds are sortable by duration", func(t2 *testing.T) { + thresholds := ServiceLevelObjectiveThresholds{ + { + TimeFrame: String("30d"), + SLO: Float64(99.9), + }, + { + TimeFrame: String("7d"), + SLO: Float64(98.9), + }, + { + TimeFrame: String("90d"), + SLO: Float64(97.9), + }, + } + + sort.Sort(thresholds) + assert.Equal(t2, "7d", thresholds[0].GetTimeFrame()) + assert.Equal(t2, "30d", thresholds[1].GetTimeFrame()) + assert.Equal(t2, "90d", thresholds[2].GetTimeFrame()) + }) + + t.Run("thresholds are comparable", func(t2 *testing.T) { + threshold1 := &ServiceLevelObjectiveThreshold{ + TimeFrame: String("30d"), + SLO: Float64(99.9), + } + threshold2 := &ServiceLevelObjectiveThreshold{ + TimeFrame: String("30d"), + SLO: Float64(99.9), + } + assert.True(t2, threshold1.Equal(threshold2)) + threshold3 := &ServiceLevelObjectiveThreshold{ + TimeFrame: String("30d"), + SLO: Float64(0.9), + } + + assert.False(t2, threshold3.Equal(threshold2)) + + threshold4 := &ServiceLevelObjectiveThreshold{ + TimeFrame: String("7d"), + SLO: Float64(99.9), + } + assert.False(t2, threshold2.Equal(threshold4)) + }) + +} diff --git a/tests/fixtures/service_level_objectives/create_response.json b/tests/fixtures/service_level_objectives/create_response.json new file mode 100644 index 0000000..918f7a7 --- /dev/null +++ b/tests/fixtures/service_level_objectives/create_response.json @@ -0,0 +1,38 @@ +{ + "data": [ + { + "id": "12345678901234567890123456789012", + "name": "Test SLO", + "tags": ["product:foo"], + "monitor_tags": ["service:bar", "team:a"], + "type": "monitor", + "type_id": 0, + "description": "test slo description", + "monitor_ids": [1], + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "jane.doe@example.com", + "email": "jane.doe@example.com", + "name": "Jane Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283800 + } + ], + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/create_response_metric.json b/tests/fixtures/service_level_objectives/create_response_metric.json new file mode 100644 index 0000000..fefbf3a --- /dev/null +++ b/tests/fixtures/service_level_objectives/create_response_metric.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "id": "abcdefabcdefabcdefabcdefabcdefab", + "name": "Test SLO 2", + "tags": ["product:foo"], + "monitor_tags": ["service:bar", "team:a"], + "type": "metric", + "type_id": 1, + "description": "test metric slo description", + "query": { + "numerator": "sum:my.metric{type:good}.as_count()", + "denominator": "sum:my.metric{*}.as_count()" + }, + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "jane.doe@example.com", + "email": "jane.doe@example.com", + "name": "Jane Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283800 + } + ], + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/delete_by_timeframe_response.json b/tests/fixtures/service_level_objectives/delete_by_timeframe_response.json new file mode 100644 index 0000000..8f866df --- /dev/null +++ b/tests/fixtures/service_level_objectives/delete_by_timeframe_response.json @@ -0,0 +1,7 @@ +{ + "data": { + "deleted": ["abcdefabcdefabcdefabcdefabcdefab"], + "updated": ["12345678901234567890123456789012"] + }, + "errors": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/delete_many_response.json b/tests/fixtures/service_level_objectives/delete_many_response.json new file mode 100644 index 0000000..524b068 --- /dev/null +++ b/tests/fixtures/service_level_objectives/delete_many_response.json @@ -0,0 +1,4 @@ +{ + "data": ["12345678901234567890123456789012", "abcdefabcdefabcdefabcdefabcdefab"], + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/delete_response.json b/tests/fixtures/service_level_objectives/delete_response.json new file mode 100644 index 0000000..327dae7 --- /dev/null +++ b/tests/fixtures/service_level_objectives/delete_response.json @@ -0,0 +1,4 @@ +{ + "data": ["12345678901234567890123456789012"], + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/get_by_id_response.json b/tests/fixtures/service_level_objectives/get_by_id_response.json new file mode 100644 index 0000000..64a70fd --- /dev/null +++ b/tests/fixtures/service_level_objectives/get_by_id_response.json @@ -0,0 +1,36 @@ +{ + "data": { + "id": "12345678901234567890123456789012", + "name": "Test SLO", + "tags": ["product:foo"], + "monitor_tags": ["service:bar", "team:a"], + "type": "monitor", + "type_id": 0, + "description": "test slo description", + "monitor_ids": [1], + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "jane.doe@example.com", + "email": "jane.doe@example.com", + "name": "Jane Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283800 + }, + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/get_many_response.json b/tests/fixtures/service_level_objectives/get_many_response.json new file mode 100644 index 0000000..c1eb9dd --- /dev/null +++ b/tests/fixtures/service_level_objectives/get_many_response.json @@ -0,0 +1,73 @@ +{ + "data": [ + { + "id": "12345678901234567890123456789012", + "name": "Test SLO", + "tags": ["product:foo"], + "monitor_tags": ["service:bar", "team:a"], + "type": "monitor", + "type_id": 0, + "description": "test slo description", + "monitor_ids": [1], + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "jane.doe@example.com", + "email": "jane.doe@example.com", + "name": "Jane Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283800 + }, + { + "id": "abcdefabcdefabcdefabcdefabcdefab", + "name": "Test SLO 2", + "tags": ["product:foo"], + "type": "metric", + "type_id": 1, + "description": "test metric slo description", + "query": { + "numerator": "sum:my.metric{type:good}.as_count()", + "denominator": "sum:my.metric{*}.as_count()" + }, + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "john.doe@example.com", + "email": "john.doe@example.com", + "name": "John Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283800 + } + ], + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/search_response.json b/tests/fixtures/service_level_objectives/search_response.json new file mode 100644 index 0000000..918f7a7 --- /dev/null +++ b/tests/fixtures/service_level_objectives/search_response.json @@ -0,0 +1,38 @@ +{ + "data": [ + { + "id": "12345678901234567890123456789012", + "name": "Test SLO", + "tags": ["product:foo"], + "monitor_tags": ["service:bar", "team:a"], + "type": "monitor", + "type_id": 0, + "description": "test slo description", + "monitor_ids": [1], + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "jane.doe@example.com", + "email": "jane.doe@example.com", + "name": "Jane Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283800 + } + ], + "error": null +} \ No newline at end of file diff --git a/tests/fixtures/service_level_objectives/update_response.json b/tests/fixtures/service_level_objectives/update_response.json new file mode 100644 index 0000000..671c784 --- /dev/null +++ b/tests/fixtures/service_level_objectives/update_response.json @@ -0,0 +1,38 @@ +{ + "data": [ + { + "id": "12345678901234567890123456789012", + "name": "Test SLO", + "tags": ["product:foo"], + "monitor_tags": ["service:bar", "team:a"], + "type": "monitor", + "type_id": 0, + "description": "test slo description", + "monitor_ids": [1], + "thresholds": [ + { + "timeframe": "7d", + "slo": 99.0, + "slo_display": "99.0", + "warning": 99.5, + "warning_display": "99.5" + }, + { + "timeframe": "30d", + "slo": 98, + "slo_display": "98.0", + "warning": 99, + "warning_display": "99.0" + } + ], + "creator": { + "handle": "jane.doe@example.com", + "email": "jane.doe@example.com", + "name": "Jane Doe" + }, + "created_at": 1563283800, + "modified_at": 1563283900 + } + ], + "error": null +} \ No newline at end of file