diff --git a/documentation/images/data-explorer-color-palette.png b/documentation/images/data-explorer-color-palette.png new file mode 100644 index 000000000..0cea7d00d Binary files /dev/null and b/documentation/images/data-explorer-color-palette.png differ diff --git a/documentation/images/data-explorer-disk-avail-thresholds.png b/documentation/images/data-explorer-disk-avail-thresholds.png new file mode 100644 index 000000000..c56a947ca Binary files /dev/null and b/documentation/images/data-explorer-disk-avail-thresholds.png differ diff --git a/documentation/images/data-explorer-service-response-time-thresholds.png b/documentation/images/data-explorer-service-response-time-thresholds.png new file mode 100644 index 000000000..ccdec6755 Binary files /dev/null and b/documentation/images/data-explorer-service-response-time-thresholds.png differ diff --git a/documentation/slis-via-dashboard.md b/documentation/slis-via-dashboard.md index 49c65d537..a864323d2 100644 --- a/documentation/slis-via-dashboard.md +++ b/documentation/slis-via-dashboard.md @@ -157,8 +157,47 @@ The following dashboard tile types are supported: ### Data explorer tiles -Data explorer tiles must only include a single query (i.e., one metric) and include up to one *filter by* and up to one *split by* clause. Metric selectors provided via the code tab are currently not supported. +Data explorer tiles must only include a single query (i.e., one metric) and include up to one *filter by* and up to one *split by* clause. Furthermore, the unit of the query must be set to `auto` (the default setting). +Metric selectors provided via the code tab are currently not supported. + +To make it easy to define SLOs using Data Explorer tiles, pass and warning criteria may be specified by adding visual thresholds directly to the tile rather than using pass and warn criteria in the tile's title. If thresholds and pass and warn criteria have been specified, the thresholds will be ignored. + + Pass-warn-fail and fail-warn-pass configurations are supported. In both cases, three thresholds must be added using strictly monotonically increasing values and colors from the pre-defined color palette: + +![Threshold colors in Data Explorer color palette](images/data-explorer-color-palette.png "Threshold colors in Data Explorer color palette") + +**Example: pass-warn-fail thresholds applied to the `builtin:service.response.time` metric** + +![Data Explorer thresholds - builtin:service.response.time](images/data-explorer-service-response-time-thresholds.png "Data Explorer thresholds - builtin:service.response.time") + +This configuration produces the following SLO criteria: + +```{yaml} +pass: + - criteria: + - ">=0" + - "<650000" +warning: + - criteria: + - ">=0" + - "<70000" +``` + +**Example: fail-warn-pass thresholds applied to the `builtin:host.disk.avail` metric** + +![Data Explorer thresholds - builtin:host.disk.avail](images/data-explorer-disk-avail-thresholds.png "Data Explorer thresholds - builtin:host.disk.avail") + +This configuration produces the following SLO criteria: + +```{yaml} +pass: + - criteria: + - ">=549755813888" +warning: + - criteria: + - ">=274877906944" +``` ### Custom chart tiles diff --git a/go.mod b/go.mod index d8c358f76..f5df1047b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/keptn/go-utils v0.16.1-0.20220628141633-eb5fb9ba43e0 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 + golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.24.3 k8s.io/apimachinery v0.24.3 @@ -30,7 +31,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.13 // indirect diff --git a/go.sum b/go.sum index eb636f55d..d724837ad 100644 --- a/go.sum +++ b/go.sum @@ -179,7 +179,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -369,6 +368,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 h1:x03zeu7B2B11ySp+daztnwM5oBJ/8wGUSqrwcw9L0RA= +golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/internal/dynatrace/dashboard.go b/internal/dynatrace/dashboard.go index b35cc5c65..f9ec82680 100644 --- a/internal/dynatrace/dashboard.go +++ b/internal/dynatrace/dashboard.go @@ -87,6 +87,30 @@ type Tile struct { AssignedEntities []string `json:"assignedEntities,omitempty"` ExcludeMaintenanceWindows bool `json:"excludeMaintenanceWindows,omitempty"` FilterConfig *FilterConfig `json:"filterConfig,omitempty"` + VisualConfig *VisualConfig `json:"visualConfig,omitempty"` +} + +// VisualConfig is the visual configuration for a dashboard tile. +type VisualConfig struct { + Thresholds []Threshold `json:"thresholds,omitempty"` + Rules []VisualConfigRule `json:"rules,omitempty"` +} + +// VisualConfigRule is a rule for the visual configuration. +type VisualConfigRule struct { + UnitTransform string `json:"unitTransform,omitempty"` +} + +// Threshold is a threshold configuration for a Data Explorer tile. +type Threshold struct { + Visible bool `json:"visible"` + Rules []ThresholdRule `json:"rules,omitempty"` +} + +// ThresholdRule is a rule for a threshold. +type ThresholdRule struct { + Value *float64 `json:"value,omitempty"` + Color string `json:"color"` } type Bounds struct { diff --git a/internal/sli/dashboard/data_explorer_thresholds.go b/internal/sli/dashboard/data_explorer_thresholds.go new file mode 100644 index 000000000..720ce3d3e --- /dev/null +++ b/internal/sli/dashboard/data_explorer_thresholds.go @@ -0,0 +1,305 @@ +package dashboard + +import ( + "errors" + "fmt" + "strings" + + "github.com/keptn-contrib/dynatrace-service/internal/dynatrace" + keptnapi "github.com/keptn/go-utils/pkg/lib" + log "github.com/sirupsen/logrus" +) + +type passAndWarningCriteria struct { + pass keptnapi.SLOCriteria + warning keptnapi.SLOCriteria +} + +type thresholdColorType int + +const ( + unknownThresholdColorType thresholdColorType = 0 + passThresholdColorType thresholdColorType = 1 + warnThresholdColorType thresholdColorType = 2 + failThresholdColorType thresholdColorType = 3 +) + +var thresholdColors = map[string]thresholdColorType{ + // pass colors + "#006613": passThresholdColorType, + "#1f7e1e": passThresholdColorType, + "#5ead35": passThresholdColorType, + "#7dc540": passThresholdColorType, + "#9cd575": passThresholdColorType, + "#e8f9dc": passThresholdColorType, + "#048855": passThresholdColorType, + "#009e60": passThresholdColorType, + "#2ab06f": passThresholdColorType, + "#54c27d": passThresholdColorType, + "#99dea8": passThresholdColorType, + "#e1f7dc": passThresholdColorType, + + // warn colors + "#ef651f": warnThresholdColorType, + "#fd8232": warnThresholdColorType, + "#ffa86c": warnThresholdColorType, + "#ffd0ab": warnThresholdColorType, + "#c9a000": warnThresholdColorType, + "#e6be00": warnThresholdColorType, + "#f5d30f": warnThresholdColorType, + "#ffe11c": warnThresholdColorType, + "#ffee7c": warnThresholdColorType, + "#fff9d5": warnThresholdColorType, + + // fail colors + "#93060e": failThresholdColorType, + "#ab0c17": failThresholdColorType, + "#c41425": failThresholdColorType, + "#dc172a": failThresholdColorType, + "#f28289": failThresholdColorType, + "#ffeaea": failThresholdColorType, +} + +func getColorType(c string) thresholdColorType { + v, ok := thresholdColors[c] + if !ok { + return unknownThresholdColorType + } + + return v +} + +func (colorType thresholdColorType) String() string { + switch colorType { + case passThresholdColorType: + return "pass" + case warnThresholdColorType: + return "warn" + case failThresholdColorType: + return "fail" + } + return "unknown" +} + +type thresholdConfiguration struct { + thresholds [3]threshold +} + +type threshold struct { + colorType thresholdColorType + value float64 +} + +type thresholdParsingErrors struct { + errors []error +} + +func (err *thresholdParsingErrors) Error() string { + var errStrings = make([]string, len(err.errors)) + for i, e := range err.errors { + errStrings[i] = e.Error() + } + return strings.Join(errStrings, "; ") +} + +type incorrectThresholdRuleCountError struct { + count int +} + +func (err *incorrectThresholdRuleCountError) Error() string { + return fmt.Sprintf("expected 3 rules rather than %d rules", err.count) +} + +type invalidThresholdColorError struct { + position int + color string +} + +func (err *invalidThresholdColorError) Error() string { + return fmt.Sprintf("invalid color %s at position %d ", err.color, err.position) +} + +type missingThresholdValueError struct { + position int +} + +func (err *missingThresholdValueError) Error() string { + return fmt.Sprintf("missing value at position %d ", err.position) +} + +type strictlyMonotonicallyIncreasingConstraintError struct { + value1 float64 + value2 float64 +} + +func (err *strictlyMonotonicallyIncreasingConstraintError) Error() string { + return fmt.Sprintf("values (%f %f) must increase strictly monotonically", err.value1, err.value2) +} + +type invalidThresholdColorSequenceError struct { + colorType1 thresholdColorType + colorType2 thresholdColorType + colorType3 thresholdColorType +} + +func (err *invalidThresholdColorSequenceError) Error() string { + return fmt.Sprintf("invalid color sequence: %s %s %s", err.colorType1, err.colorType2, err.colorType3) +} + +// tryGetThresholdPassAndWarningCriteria tries to get pass and warning criteria defined using the thresholds placed on a Data Explorer tile. +// It returns either the criteria and no error (conversion succeeded), nil for the criteria and no error (no threshold set), or nil for the criteria and an error (conversion failed). +func tryGetThresholdPassAndWarningCriteria(tile *dynatrace.Tile) (*passAndWarningCriteria, error) { + if tile.VisualConfig == nil { + return nil, nil + } + + visualConfig := tile.VisualConfig + if len(visualConfig.Thresholds) == 0 { + return nil, nil + } + + if len(visualConfig.Thresholds) > 1 { + return nil, errors.New("too many threshold configurations") + } + + t := &visualConfig.Thresholds[0] + if !areThresholdsEnabled(t) { + return nil, nil + } + + thresholdConfiguration, err := convertThresholdRulesToThresholdConfiguration(t.Rules) + if err != nil { + return nil, err + } + + return convertThresholdConfigurationToPassAndWarningCriteria(*thresholdConfiguration) +} + +// areThresholdsEnabled returns true if a user has set thresholds that will be displayed, i.e. if thresholds are visible and at least one value has been set. +func areThresholdsEnabled(threshold *dynatrace.Threshold) bool { + if !threshold.Visible { + return false + } + + for _, rule := range threshold.Rules { + if rule.Value != nil { + return true + } + } + + return false +} + +// convertThresholdRulesToThresholdConfiguration checks that the threshold rules are complete and returns them as a threshold configuration or returns an error. +func convertThresholdRulesToThresholdConfiguration(rules []dynatrace.ThresholdRule) (*thresholdConfiguration, error) { + var errs []error + + if len(rules) != 3 { + // log this error as it may mean something has changed on the Data Explorer side + log.WithField("ruleCount", len(rules)).Error("Encountered unexpected number of threshold rules") + + errs = append(errs, &incorrectThresholdRuleCountError{count: len(rules)}) + } + + for i, rule := range rules { + if rule.Value == nil { + errs = append(errs, &missingThresholdValueError{position: i + 1}) + } + + if getColorType(rule.Color) == unknownThresholdColorType { + errs = append(errs, &invalidThresholdColorError{color: rule.Color, position: i + 1}) + } + } + + if len(errs) > 0 { + return nil, &thresholdParsingErrors{errors: errs} + } + + return &thresholdConfiguration{ + thresholds: [3]threshold{ + {colorType: getColorType(rules[0].Color), value: *rules[0].Value}, + {colorType: getColorType(rules[1].Color), value: *rules[1].Value}, + {colorType: getColorType(rules[2].Color), value: *rules[2].Value}}}, nil +} + +func convertThresholdConfigurationToPassAndWarningCriteria(t thresholdConfiguration) (*passAndWarningCriteria, error) { + var errs []error + + v1 := t.thresholds[0].value + v2 := t.thresholds[1].value + v3 := t.thresholds[2].value + + if v1 >= v2 { + errs = append(errs, &strictlyMonotonicallyIncreasingConstraintError{value1: v1, value2: v2}) + } + + if v2 >= v3 { + errs = append(errs, &strictlyMonotonicallyIncreasingConstraintError{value1: v2, value2: v3}) + } + + sloCriteria, err := matchThresholdColorSequenceAndConvertToPassAndWarningCriteria(t) + if err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return nil, &thresholdParsingErrors{errors: errs} + } + + return sloCriteria, nil +} + +func matchThresholdColorSequenceAndConvertToPassAndWarningCriteria(t thresholdConfiguration) (*passAndWarningCriteria, error) { + colorType1 := t.thresholds[0].colorType + colorType2 := t.thresholds[1].colorType + colorType3 := t.thresholds[2].colorType + + if (colorType1 == passThresholdColorType) && (colorType2 == warnThresholdColorType) && (colorType3 == failThresholdColorType) { + return convertPassWarnFailThresholdsToPassAndWarningCriteria(t), nil + } + + if (colorType1 == failThresholdColorType) && (colorType2 == warnThresholdColorType) && (colorType3 == passThresholdColorType) { + return convertFailWarnPassThresholdsToPassAndWarningCriteria(t), nil + } + + return nil, &invalidThresholdColorSequenceError{colorType1: colorType1, colorType2: colorType2, colorType3: colorType3} +} + +func convertPassWarnFailThresholdsToPassAndWarningCriteria(t thresholdConfiguration) *passAndWarningCriteria { + passThreshold := t.thresholds[0].value + warnThreshold := t.thresholds[1].value + failThreshold := t.thresholds[2].value + + return &passAndWarningCriteria{ + pass: keptnapi.SLOCriteria{ + Criteria: []string{ + fmt.Sprintf(">=%f", passThreshold), + fmt.Sprintf("<%f", warnThreshold), + }, + }, + warning: keptnapi.SLOCriteria{ + Criteria: []string{ + fmt.Sprintf(">=%f", passThreshold), + fmt.Sprintf("<%f", failThreshold), + }, + }, + } +} + +func convertFailWarnPassThresholdsToPassAndWarningCriteria(t thresholdConfiguration) *passAndWarningCriteria { + warnThreshold := t.thresholds[1].value + passThreshold := t.thresholds[2].value + + return &passAndWarningCriteria{ + pass: keptnapi.SLOCriteria{ + Criteria: []string{ + fmt.Sprintf(">=%f", passThreshold), + }, + }, + warning: keptnapi.SLOCriteria{ + Criteria: []string{ + fmt.Sprintf(">=%f", warnThreshold), + }, + }, + } +} diff --git a/internal/sli/dashboard/data_explorer_tile_processing.go b/internal/sli/dashboard/data_explorer_tile_processing.go index 2b6ed93ef..e06ba950e 100644 --- a/internal/sli/dashboard/data_explorer_tile_processing.go +++ b/internal/sli/dashboard/data_explorer_tile_processing.go @@ -53,8 +53,21 @@ func (p *DataExplorerTileProcessing) Process(ctx context.Context, tile *dynatrac return nil } - if len(tile.Queries) != 1 { - return []TileResult{newFailedTileResultFromSLODefinition(sloDefinition, "Data Explorer tile must have exactly one query")} + if (len(sloDefinition.Pass) == 0) && (len(sloDefinition.Warning) == 0) { + criteria, err := tryGetThresholdPassAndWarningCriteria(tile) + if err != nil { + return []TileResult{newFailedTileResultFromSLODefinition(sloDefinition, fmt.Sprintf("Invalid Data Explorer tile thresholds: %s", err.Error()))} + } + + if criteria != nil { + sloDefinition.Pass = []*keptnapi.SLOCriteria{&criteria.pass} + sloDefinition.Warning = []*keptnapi.SLOCriteria{&criteria.warning} + } + } + + err = validateDataExplorerTile(tile) + if err != nil { + return []TileResult{newFailedTileResultFromSLODefinition(sloDefinition, err.Error())} } // get the tile specific management zone filter that might be needed by different tile processors @@ -64,6 +77,33 @@ func (p *DataExplorerTileProcessing) Process(ctx context.Context, tile *dynatrac return p.processQuery(ctx, sloDefinition, tile.Queries[0], managementZoneFilter) } +func validateDataExplorerTile(tile *dynatrace.Tile) error { + if len(tile.Queries) != 1 { + return fmt.Errorf("Data Explorer tile must have exactly one query") + } + + if tile.VisualConfig == nil { + return nil + } + + if len(tile.VisualConfig.Rules) == 0 { + return nil + } + + if len(tile.VisualConfig.Rules) > 1 { + return fmt.Errorf("Data Explorer tile must have exactly one visual configuration rule") + } + + return validateDataExplorerVisualConfigurationRule(tile.VisualConfig.Rules[0]) +} + +func validateDataExplorerVisualConfigurationRule(rule dynatrace.VisualConfigRule) error { + if rule.UnitTransform != "" { + return fmt.Errorf("Data Explorer query unit must be set to 'Auto' rather than '%s'", rule.UnitTransform) + } + return nil +} + func (p *DataExplorerTileProcessing) processQuery(ctx context.Context, sloDefinition keptnapi.SLO, dataQuery dynatrace.DataExplorerQuery, managementZoneFilter *ManagementZoneFilter) []TileResult { log.WithField("metric", dataQuery.Metric).Debug("Processing data explorer query") diff --git a/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_test.go b/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_test.go index b2be9ca4f..e2bf1ccfd 100644 --- a/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_test.go +++ b/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_test.go @@ -1,6 +1,7 @@ package sli import ( + "fmt" "testing" keptn "github.com/keptn/go-utils/pkg/lib" @@ -486,3 +487,106 @@ func TestRetrieveMetricsFromDashboardDataExplorerTile_ExcludedTile(t *testing.T) runGetSLIsFromDashboardTestAndCheckSLIs(t, handler, testGetSLIEventData, getSLIFinishedEventSuccessAssertionsFunc, sliResultsAssertionsFuncs...) } + +// TestRetrieveMetricsFromDashboardDataExplorerTile_TileThresholdsWork tests that setting pass and warning criteria via thresholds on the tile works as expected. +func TestRetrieveMetricsFromDashboardDataExplorerTile_TileThresholdsWork(t *testing.T) { + const testDataFolder = "./testdata/dashboards/data_explorer/tile_thresholds_success/" + + expectedMetricsRequest := buildMetricsV2RequestString("builtin%3Aservice.response.time%3AsplitBy%28%29%3Aavg%3Anames") + + successfulSLIResultAllectionsFunc := createSuccessfulSLIResultAssertionsFunc("srt", 29192.929640271974, expectedMetricsRequest) + + tests := []struct { + name string + dashboardFilename string + + expectedSLO *keptnapi.SLO + }{ + { + name: "Valid pass-warn-fail thresholds and no pass or warning defined in title", + dashboardFilename: testDataFolder + "dashboard_just_thresholds_pass_warn_fail.json", + expectedSLO: createExpectedServiceResponseTimeSLO(createBandSLOCriteria(0, 68000), createBandSLOCriteria(0, 69000)), + }, + { + name: "Valid fail-warn-pass thresholds and no pass or warning defined in title", + dashboardFilename: testDataFolder + "dashboard_just_thresholds_fail_warn_pass.json", + expectedSLO: createExpectedServiceResponseTimeSLO(createLowerBoundSLOCriteria(69000), createLowerBoundSLOCriteria(68000)), + }, + { + name: "Pass or warning defined in title take precedence over valid thresholds ", + dashboardFilename: testDataFolder + "dashboard_both_thresholds_and_pass_and_warning_in_title.json", + expectedSLO: createExpectedServiceResponseTimeSLO( + []*keptnapi.SLOCriteria{{Criteria: []string{"<70000"}}}, + []*keptnapi.SLOCriteria{{Criteria: []string{"<71000"}}}), + }, + { + name: "Visible thresholds with no values are ignored", + dashboardFilename: testDataFolder + "dashboard_visible_thresholds_without_values.json", + expectedSLO: createExpectedServiceResponseTimeSLO(nil, nil), + }, + { + name: "Not visible thresholds with valid values are ignored", + dashboardFilename: testDataFolder + "dashboard_not_visible_thresholds_with_valid_values.json", + expectedSLO: createExpectedServiceResponseTimeSLO(nil, nil), + }, + { + name: "Not visible thresholds with invalid values are ignored", + dashboardFilename: testDataFolder + "dashboard_not_visible_thresholds_with_invalid_values.json", + expectedSLO: createExpectedServiceResponseTimeSLO(nil, nil), + }, + } + + for _, thresholdTest := range tests { + t.Run(thresholdTest.name, func(t *testing.T) { + + handler := test.NewFileBasedURLHandler(t) + handler.AddExact(dynatrace.DashboardsPath+"/"+testDashboardID, thresholdTest.dashboardFilename) + handler.AddExact(dynatrace.MetricsPath+"/builtin:service.response.time", testDataFolder+"metrics_builtin_service_response_time.json") + handler.AddExact(expectedMetricsRequest, testDataFolder+"metrics_query_builtin_service_response_time_avg.json") + + uploadedSLOsAssertionsFunc := func(t *testing.T, actual *keptn.ServiceLevelObjectives) { + if assert.Equal(t, 1, len(actual.Objectives)) { + assert.EqualValues(t, thresholdTest.expectedSLO, actual.Objectives[0]) + } + } + + runGetSLIsFromDashboardTestAndCheckSLIsAndSLOs(t, handler, testGetSLIEventData, getSLIFinishedEventSuccessAssertionsFunc, uploadedSLOsAssertionsFunc, successfulSLIResultAllectionsFunc) + }) + } +} + +// TestRetrieveMetricsFromDashboardDataExplorerTile_UnitTransformIsNotAuto tests that unit transforms other than auto are not allowed. +// This is will result in a SLIResult with failure, as this is not allowed. +func TestRetrieveMetricsFromDashboardDataExplorerTile_UnitTransformIsNotAuto(t *testing.T) { + handler := test.NewFileBasedURLHandler(t) + handler.AddExact(dynatrace.DashboardsPath+"/"+testDashboardID, "./testdata/dashboards/data_explorer/unit_transform_is_not_auto/dashboard.json") + + runGetSLIsFromDashboardTestAndCheckSLIs(t, handler, testGetSLIEventData, getSLIFinishedEventFailureAssertionsFunc, createFailedSLIResultAssertionsFunc("srt", "must be set to 'Auto'")) +} + +func createExpectedServiceResponseTimeSLO(passCriteria []*keptnapi.SLOCriteria, warningCriteria []*keptnapi.SLOCriteria) *keptnapi.SLO { + return &keptnapi.SLO{ + SLI: "srt", + DisplayName: "Service Response Time", + Pass: passCriteria, + Warning: warningCriteria, + Weight: 1, + KeySLI: false, + } +} + +func createBandSLOCriteria(lowerBoundInclusive float64, upperBoundExclusive float64) []*keptnapi.SLOCriteria { + return []*keptnapi.SLOCriteria{{Criteria: []string{createGreaterThanOrEqualSLOCriterion(lowerBoundInclusive), createLessThanSLOCriterion(upperBoundExclusive)}}} +} + +func createLowerBoundSLOCriteria(lowerBoundInclusive float64) []*keptnapi.SLOCriteria { + return []*keptnapi.SLOCriteria{{Criteria: []string{createGreaterThanOrEqualSLOCriterion(lowerBoundInclusive)}}} +} + +func createGreaterThanOrEqualSLOCriterion(v float64) string { + return fmt.Sprintf(">=%f", v) +} + +func createLessThanSLOCriterion(v float64) string { + return fmt.Sprintf("<%f", v) +} diff --git a/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_threshold_errors_test.go b/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_threshold_errors_test.go new file mode 100644 index 000000000..c366578a8 --- /dev/null +++ b/internal/sli/get_sli_triggered_event_handler_retrieve_metrics_from_dashboard_data_explorer_threshold_errors_test.go @@ -0,0 +1,211 @@ +package sli + +import ( + "fmt" + "testing" + + "github.com/keptn-contrib/dynatrace-service/internal/dynatrace" + "github.com/keptn-contrib/dynatrace-service/internal/test" +) + +type dataExplorerThresholdErrorsTest struct { + name string + thresholdValues []*float64 + thresholdColors []string + sliResultAssertionsFunc func(t *testing.T, actual sliResult) +} + +type tileThresholdsTemplateData struct { + ThresholdValues []*float64 + ThresholdColors []string +} + +const ( + missingValueErrorSubstring = "missing value" + invalidColorErrorSubstring = "invalid color" + expected3RulesErrorSubstring = "expected 3 rules" + invalidColorSequenceErrorSubstring = "invalid color sequence" + expectedMonotonicallyIncreasingErrorSubstring = "must increase strictly monotonically" + atPosition1ErrorSubstring = "at position 1" + atPosition2ErrorSubstring = "at position 2" + atPosition3ErrorSubstring = "at position 3" +) + +const ( + invalidThresholdColor = "#14a8f5" + passThresholdColor = "#7dc540" + warnThresholdColor = "#f5d30f" + failThresholdColor = "#dc172a" +) + +var passWarnFailThresholdColors = []string{passThresholdColor, warnThresholdColor, failThresholdColor} + +var thresholdValue0 float64 = 0 +var thresholdValue68000 float64 = 68000 +var thresholdValue69000 float64 = 69000 + +var validThresholdValues = []*float64{&thresholdValue0, &thresholdValue68000, &thresholdValue69000} + +// TestRetrieveMetricsFromDashboardDataExplorerTile_TileThresholdRuleParsingErrors tests that errors while parsing Data Explorer tile thresholds are generated as expected. +// Includes tests with multiple errors are these should all be included in the overall error message. +func TestRetrieveMetricsFromDashboardDataExplorerTile_TileThresholdRuleParsingErrors(t *testing.T) { + const testDataFolder = "./testdata/dashboards/data_explorer/tile_thresholds_errors/" + + tests := []struct { + name string + thresholdValues []*float64 + thresholdColors []string + sliResultAssertionsFunc func(t *testing.T, actual sliResult) + }{ + // Rule count + { + name: "Too few rules in thresholds", + thresholdValues: []*float64{&thresholdValue0, &thresholdValue69000}, + thresholdColors: []string{passThresholdColor, warnThresholdColor}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", expected3RulesErrorSubstring), + }, + { + name: "Too many rules in thresholds", + thresholdValues: []*float64{&thresholdValue0, &thresholdValue68000, &thresholdValue69000, &thresholdValue69000}, + thresholdColors: []string{passThresholdColor, warnThresholdColor, failThresholdColor, failThresholdColor}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", expected3RulesErrorSubstring), + }, + + // Missing values + createDataExplorerThresholdsErrorTestWithMissingValues("Missing value at position 1", nil, &thresholdValue68000, &thresholdValue69000, atPosition1ErrorSubstring), + createDataExplorerThresholdsErrorTestWithMissingValues("Missing value at position 2", &thresholdValue0, nil, &thresholdValue69000, atPosition2ErrorSubstring), + createDataExplorerThresholdsErrorTestWithMissingValues("Missing value at position 3", &thresholdValue0, &thresholdValue68000, nil, atPosition3ErrorSubstring), + createDataExplorerThresholdsErrorTestWithMissingValues("Missing values at position 1 and 2", nil, nil, &thresholdValue69000, atPosition1ErrorSubstring, atPosition2ErrorSubstring), + createDataExplorerThresholdsErrorTestWithMissingValues("Missing values at position 2 and 3", &thresholdValue0, nil, nil, atPosition2ErrorSubstring, atPosition3ErrorSubstring), + createDataExplorerThresholdsErrorTestWithMissingValues("Missing values at position 1 and 3", nil, &thresholdValue68000, nil, atPosition1ErrorSubstring, atPosition3ErrorSubstring), + + // Combined rule count and missing value + { + name: "Too many rules in thresholds and missing value", + thresholdValues: []*float64{&thresholdValue0, &thresholdValue68000, nil, &thresholdValue69000}, + thresholdColors: []string{passThresholdColor, warnThresholdColor, failThresholdColor, failThresholdColor}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", expected3RulesErrorSubstring, missingValueErrorSubstring, atPosition3ErrorSubstring), + }, + + // Invalid color + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 1", invalidThresholdColor, warnThresholdColor, failThresholdColor, atPosition1ErrorSubstring), + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 2", passThresholdColor, invalidThresholdColor, failThresholdColor, atPosition2ErrorSubstring), + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 3", passThresholdColor, warnThresholdColor, invalidThresholdColor, atPosition3ErrorSubstring), + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 1 and 2", invalidThresholdColor, invalidThresholdColor, failThresholdColor, atPosition1ErrorSubstring, atPosition2ErrorSubstring), + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 2 and 3", passThresholdColor, invalidThresholdColor, invalidThresholdColor, atPosition2ErrorSubstring, atPosition3ErrorSubstring), + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 1 and 3", invalidThresholdColor, warnThresholdColor, invalidThresholdColor, atPosition1ErrorSubstring, atPosition3ErrorSubstring), + createDataExplorerThresholdsErrorTestWithInvalidColors("Invalid color at position 1, 2 and 3", invalidThresholdColor, invalidThresholdColor, invalidThresholdColor, atPosition1ErrorSubstring, atPosition2ErrorSubstring, atPosition3ErrorSubstring), + + // Combined invalid color and missing value + { + name: "Invalid color at position 1 and missing value at position 2", + thresholdValues: []*float64{&thresholdValue0, nil, &thresholdValue69000}, + thresholdColors: []string{invalidThresholdColor, warnThresholdColor, failThresholdColor}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", invalidColorErrorSubstring, atPosition1ErrorSubstring, missingValueErrorSubstring, atPosition2ErrorSubstring), + }, + + // Combined invalid color, missing value and too many rules + { + name: "Invalid color at position 1 and missing value at position 2 and too many rules", + thresholdValues: []*float64{&thresholdValue0, nil, &thresholdValue69000, &thresholdValue69000}, + thresholdColors: []string{invalidThresholdColor, warnThresholdColor, failThresholdColor, failThresholdColor}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", invalidColorErrorSubstring, atPosition1ErrorSubstring, missingValueErrorSubstring, atPosition2ErrorSubstring, expected3RulesErrorSubstring), + }, + + // Invalid color sequences + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-pass-pass sequence", passThresholdColor, passThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-pass-pass sequence", warnThresholdColor, passThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-pass-pass sequence", failThresholdColor, passThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-warn-pass sequence", passThresholdColor, warnThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-warn-pass sequence", warnThresholdColor, warnThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-fail-pass sequence", passThresholdColor, failThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-fail-pass sequence", warnThresholdColor, failThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-fail-pass sequence", failThresholdColor, failThresholdColor, passThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-pass-warn sequence", passThresholdColor, passThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-pass-warn sequence", warnThresholdColor, passThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-pass-warn sequence", failThresholdColor, passThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-warn-warn sequence", passThresholdColor, warnThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-warn-warn sequence", warnThresholdColor, warnThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-warn-warn sequence", failThresholdColor, warnThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-fail-warn sequence", passThresholdColor, failThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-fail-warn sequence", warnThresholdColor, failThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-fail-warn sequence", failThresholdColor, failThresholdColor, warnThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-pass-fail sequence", passThresholdColor, passThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-pass-fail sequence", warnThresholdColor, passThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-pass-fail sequence", failThresholdColor, passThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-warn-fail sequence", warnThresholdColor, warnThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-warn-fail sequence", failThresholdColor, warnThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid pass-fail-fail sequence", passThresholdColor, failThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid warn-fail-fail sequence", warnThresholdColor, failThresholdColor, failThresholdColor), + createDataExplorerThresholdsErrorTestWithColorSequence("Invalid fail-fail-fail sequence", failThresholdColor, failThresholdColor, failThresholdColor), + + // Not strictly monotonically increasing values + // Redundant cases have been removed + createDataExplorerThresholdsErrorTestWithWrongValues(0, 0, 0), // *-same-same + createDataExplorerThresholdsErrorTestWithWrongValues(0, 0, 68000), // *-same-higher + createDataExplorerThresholdsErrorTestWithWrongValues(0, 68000, 68000), // *-higher-higher + createDataExplorerThresholdsErrorTestWithWrongValues(0, 68000, 0), // *-higher-same + createDataExplorerThresholdsErrorTestWithWrongValues(0, 69000, 68000), // *-even_higher-higher + createDataExplorerThresholdsErrorTestWithWrongValues(68000, 0, 0), // *-lower-lower + createDataExplorerThresholdsErrorTestWithWrongValues(68000, 0, 68000), // *-lower-same + createDataExplorerThresholdsErrorTestWithWrongValues(68000, 0, 69000), // *-lower-higher + createDataExplorerThresholdsErrorTestWithWrongValues(68000, 68000, 0), // *-same-lower + createDataExplorerThresholdsErrorTestWithWrongValues(68000, 69000, 0), // *-higher-lower + createDataExplorerThresholdsErrorTestWithWrongValues(69000, 0, 68000), // *-even_lower-lower + createDataExplorerThresholdsErrorTestWithWrongValues(69000, 68000, 0), // *-lower-even_lower + + // Combined invalid color sequence and not strictly monotonically increasing values + { + name: "Invalid color sequence and not strictly monotonically increasing values", + thresholdValues: []*float64{&thresholdValue68000, &thresholdValue69000, &thresholdValue69000}, + thresholdColors: []string{warnThresholdColor, warnThresholdColor, failThresholdColor}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", invalidColorSequenceErrorSubstring, expectedMonotonicallyIncreasingErrorSubstring), + }, + } + + for _, thresholdTest := range tests { + t.Run(thresholdTest.name, func(t *testing.T) { + handler := test.NewTemplatingPayloadBasedURLHandler(t, testDataFolder+"dashboard_thresholds_template.json") + handler.AddExact(dynatrace.DashboardsPath+"/"+testDashboardID, tileThresholdsTemplateData{ThresholdValues: thresholdTest.thresholdValues, ThresholdColors: thresholdTest.thresholdColors}) + + runGetSLIsFromDashboardTestAndCheckSLIs(t, handler, testGetSLIEventData, getSLIFinishedEventFailureAssertionsFunc, thresholdTest.sliResultAssertionsFunc) + }) + } +} + +func createDataExplorerThresholdsErrorTestWithMissingValues(name string, v1 *float64, v2 *float64, v3 *float64, positionErrorSubstrings ...string) dataExplorerThresholdErrorsTest { + return createDataExplorerThresholdsErrorTestWithValues(name, v1, v2, v3, missingValueErrorSubstring, positionErrorSubstrings...) +} + +func createDataExplorerThresholdsErrorTestWithWrongValues(v1 float64, v2 float64, v3 float64) dataExplorerThresholdErrorsTest { + name := fmt.Sprintf("Invalid not strictly monotonically increasing values test %f %f %f", v1, v2, v3) + return createDataExplorerThresholdsErrorTestWithValues(name, &v1, &v2, &v3, expectedMonotonicallyIncreasingErrorSubstring) +} + +func createDataExplorerThresholdsErrorTestWithValues(name string, v1 *float64, v2 *float64, v3 *float64, mainErrorSubstring string, positionErrorSubstrings ...string) dataExplorerThresholdErrorsTest { + expectedErrorSubstrings := append([]string{mainErrorSubstring}, positionErrorSubstrings...) + return dataExplorerThresholdErrorsTest{ + name: name, + thresholdValues: []*float64{v1, v2, v3}, + thresholdColors: passWarnFailThresholdColors, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", expectedErrorSubstrings...), + } +} + +func createDataExplorerThresholdsErrorTestWithInvalidColors(name string, c1 string, c2 string, c3 string, positionErrorSubstrings ...string) dataExplorerThresholdErrorsTest { + return createDataExplorerThresholdsErrorTestWithColors(name, c1, c2, c3, invalidColorErrorSubstring, positionErrorSubstrings...) +} + +func createDataExplorerThresholdsErrorTestWithColorSequence(name string, c1 string, c2 string, c3 string) dataExplorerThresholdErrorsTest { + return createDataExplorerThresholdsErrorTestWithColors(name, c1, c2, c3, invalidColorSequenceErrorSubstring) +} + +func createDataExplorerThresholdsErrorTestWithColors(name string, c1 string, c2 string, c3 string, mainErrorSubstring string, positionErrorSubstrings ...string) dataExplorerThresholdErrorsTest { + expectedErrorSubstrings := append([]string{mainErrorSubstring}, positionErrorSubstrings...) + return dataExplorerThresholdErrorsTest{ + name: name, + thresholdValues: validThresholdValues, + thresholdColors: []string{c1, c2, c3}, + sliResultAssertionsFunc: createFailedSLIResultAssertionsFunc("srt", expectedErrorSubstrings...), + } +} diff --git a/internal/sli/testdata/dashboards/data_explorer/no_spaceag_no_filterby/dashboard_no_spaceag_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/no_spaceag_no_filterby/dashboard_no_spaceag_no_filterby.json index 6aae60835..3faed6100 100644 --- a/internal/sli/testdata/dashboards/data_explorer/no_spaceag_no_filterby/dashboard_no_spaceag_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/no_spaceag_no_filterby/dashboard_no_spaceag_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] - } \ No newline at end of file + } diff --git a/internal/sli/testdata/dashboards/data_explorer/spaceag_median_no_filterby/dashboard_spaceag_median_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/spaceag_median_no_filterby/dashboard_spaceag_median_no_filterby.json index 7ac216ae6..397de7d6d 100644 --- a/internal/sli/testdata/dashboards/data_explorer/spaceag_median_no_filterby/dashboard_spaceag_median_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/spaceag_median_no_filterby/dashboard_spaceag_median_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/sli/testdata/dashboards/data_explorer/spaceag_min_no_filterby/dashboard_spaceag_min_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/spaceag_min_no_filterby/dashboard_spaceag_min_no_filterby.json index 408db0567..030833192 100644 --- a/internal/sli/testdata/dashboards/data_explorer/spaceag_min_no_filterby/dashboard_spaceag_min_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/spaceag_min_no_filterby/dashboard_spaceag_min_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/sli/testdata/dashboards/data_explorer/spaceag_p10_no_filterby/dashboard_spaceag_p10_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/spaceag_p10_no_filterby/dashboard_spaceag_p10_no_filterby.json index 0ebf3f989..a258acdad 100644 --- a/internal/sli/testdata/dashboards/data_explorer/spaceag_p10_no_filterby/dashboard_spaceag_p10_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/spaceag_p10_no_filterby/dashboard_spaceag_p10_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/sli/testdata/dashboards/data_explorer/spaceag_p75_no_filterby/dashboard_spaceag_p75_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/spaceag_p75_no_filterby/dashboard_spaceag_p75_no_filterby.json index 7db954d43..74595bf58 100644 --- a/internal/sli/testdata/dashboards/data_explorer/spaceag_p75_no_filterby/dashboard_spaceag_p75_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/spaceag_p75_no_filterby/dashboard_spaceag_p75_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/sli/testdata/dashboards/data_explorer/spaceag_p90_no_filterby/dashboard_spaceag_p90_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/spaceag_p90_no_filterby/dashboard_spaceag_p90_no_filterby.json index ff4c9caea..b6458dbb9 100644 --- a/internal/sli/testdata/dashboards/data_explorer/spaceag_p90_no_filterby/dashboard_spaceag_p90_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/spaceag_p90_no_filterby/dashboard_spaceag_p90_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] -} \ No newline at end of file +} diff --git a/internal/sli/testdata/dashboards/data_explorer/spaceag_sum_no_filterby/dashboard_spaceag_sum_no_filterby.json b/internal/sli/testdata/dashboards/data_explorer/spaceag_sum_no_filterby/dashboard_spaceag_sum_no_filterby.json index cb6351adf..6fd1c3909 100644 --- a/internal/sli/testdata/dashboards/data_explorer/spaceag_sum_no_filterby/dashboard_spaceag_sum_no_filterby.json +++ b/internal/sli/testdata/dashboards/data_explorer/spaceag_sum_no_filterby/dashboard_spaceag_sum_no_filterby.json @@ -59,7 +59,6 @@ "axisTarget": "LEFT", "rules": [ { - "value": 1, "color": "#7dc540" }, { @@ -82,4 +81,4 @@ } } ] - } \ No newline at end of file + } diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_errors/dashboard_thresholds_template.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_errors/dashboard_thresholds_template.json new file mode 100644 index 000000000..6b0884d7a --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_errors/dashboard_thresholds_template.json @@ -0,0 +1,111 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [{{$first := true}}{{$colors := .ThresholdColors}}{{range $i, $e := .ThresholdValues}}{{if $first}}{{$first = false}}{{else}},{{end}} + { + {{if not $e}}{{else}}"value":{{$e}},{{end}} + "color": "{{index $colors $i}}" + }{{end}} + ], + "queryId": "", + "visible": true + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_both_thresholds_and_pass_and_warning_in_title.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_both_thresholds_and_pass_and_warning_in_title.json new file mode 100644 index 000000000..91b841fc9 --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_both_thresholds_and_pass_and_warning_in_title.json @@ -0,0 +1,119 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt; pass=<70000; warning=<71000", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "value": 0, + "color": "#7dc540" + }, + { + "value": 68000, + "color": "#f5d30f" + }, + { + "value": 69000, + "color": "#dc172a" + } + ], + "queryId": "", + "visible": true + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_just_thresholds_fail_warn_pass.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_just_thresholds_fail_warn_pass.json new file mode 100644 index 000000000..454a9d047 --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_just_thresholds_fail_warn_pass.json @@ -0,0 +1,119 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "value": 0, + "color": "#dc172a" + }, + { + "value": 68000, + "color": "#f5d30f" + }, + { + "value": 69000, + "color": "#7dc540" + } + ], + "queryId": "", + "visible": true + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_just_thresholds_pass_warn_fail.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_just_thresholds_pass_warn_fail.json new file mode 100644 index 000000000..cf01ee4e9 --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_just_thresholds_pass_warn_fail.json @@ -0,0 +1,119 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "value": 0, + "color": "#7dc540" + }, + { + "value": 68000, + "color": "#f5d30f" + }, + { + "value": 69000, + "color": "#dc172a" + } + ], + "queryId": "", + "visible": true + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_not_visible_thresholds_with_invalid_values.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_not_visible_thresholds_with_invalid_values.json new file mode 100644 index 000000000..fc3c4caf0 --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_not_visible_thresholds_with_invalid_values.json @@ -0,0 +1,119 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "value": 0, + "color": "#7dc540" + }, + { + "value": 68000, + "color": "#f5d30f" + }, + { + "value": 68000, + "color": "#dc172a" + } + ], + "queryId": "", + "visible": false + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_not_visible_thresholds_with_valid_values.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_not_visible_thresholds_with_valid_values.json new file mode 100644 index 000000000..0d4ed9b0f --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_not_visible_thresholds_with_valid_values.json @@ -0,0 +1,119 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "value": 0, + "color": "#7dc540" + }, + { + "value": 68000, + "color": "#f5d30f" + }, + { + "value": 69000, + "color": "#dc172a" + } + ], + "queryId": "", + "visible": false + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_visible_thresholds_without_values.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_visible_thresholds_without_values.json new file mode 100644 index 000000000..3efa9e6a3 --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/dashboard_visible_thresholds_without_values.json @@ -0,0 +1,116 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "properties": { + "color": "DEFAULT" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "color": "#7dc540" + }, + { + "color": "#f5d30f" + }, + { + "color": "#dc172a" + } + ], + "queryId": "", + "visible": true + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +} diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/metrics_builtin_service_response_time.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/metrics_builtin_service_response_time.json new file mode 100644 index 000000000..4d2fccfbc --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/metrics_builtin_service_response_time.json @@ -0,0 +1,53 @@ +{ + "metricId": "builtin:service.response.time", + "displayName": "Response time", + "description": "", + "unit": "MicroSecond", + "dduBillable": false, + "created": 0, + "lastWritten": 1637320720982, + "entityType": [ + "SERVICE" + ], + "aggregationTypes": [ + "auto", + "avg", + "count", + "max", + "median", + "min", + "percentile", + "sum" + ], + "transformations": [ + "filter", + "fold", + "limit", + "merge", + "names", + "parents", + "timeshift", + "sort", + "last", + "splitBy", + "lastReal" + ], + "defaultAggregation": { + "type": "avg" + }, + "dimensionDefinitions": [ + { + "key": "dt.entity.service", + "name": "Service", + "displayName": "Service", + "index": 0, + "type": "ENTITY" + } + ], + "tags": [], + "metricValueType": { + "type": "unknown" + }, + "scalar": false, + "resolutionInfSupported": true +} \ No newline at end of file diff --git a/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/metrics_query_builtin_service_response_time_avg.json b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/metrics_query_builtin_service_response_time_avg.json new file mode 100644 index 000000000..fb4256add --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/tile_thresholds_success/metrics_query_builtin_service_response_time_avg.json @@ -0,0 +1,22 @@ +{ + "totalCount": 1, + "nextPageKey": null, + "resolution": "Inf", + "result": [ + { + "metricId": "builtin:service.response.time:splitBy():avg:names", + "data": [ + { + "dimensions": [], + "dimensionMap": {}, + "timestamps": [ + 1609545600000 + ], + "values": [ + 29192.929640271974 + ] + } + ] + } + ] + } \ No newline at end of file diff --git a/internal/sli/testdata/dashboards/data_explorer/unit_transform_is_not_auto/dashboard.json b/internal/sli/testdata/dashboards/data_explorer/unit_transform_is_not_auto/dashboard.json new file mode 100644 index 000000000..d7e092279 --- /dev/null +++ b/internal/sli/testdata/dashboards/data_explorer/unit_transform_is_not_auto/dashboard.json @@ -0,0 +1,122 @@ +{ + "metadata": { + "configurationVersions": [ + 5 + ], + "clusterVersion": "1.245.0.20220804-195908" + }, + "id": "12345678-1111-4444-8888-123456789012", + "dashboardMetadata": { + "name": "", + "shared": false, + "owner": "", + "popularity": 1 + }, + "tiles": [ + { + "name": "Service Response Time; sli=srt", + "tileType": "DATA_EXPLORER", + "configured": true, + "bounds": { + "top": 266, + "left": 0, + "width": 1140, + "height": 190 + }, + "tileFilter": {}, + "customName": "Data explorer results", + "queries": [ + { + "id": "A", + "metric": "builtin:service.response.time", + "spaceAggregation": "AVG", + "timeAggregation": "DEFAULT", + "splitBy": [], + "filterBy": { + "nestedFilters": [], + "criteria": [] + }, + "enabled": true + } + ], + "visualConfig": { + "type": "GRAPH_CHART", + "global": { + "hideLegend": false + }, + "rules": [ + { + "matcher": "A:", + "unitTransform": "MilliSecond", + "valueFormat": "auto", + "properties": { + "color": "DEFAULT", + "seriesType": "LINE" + }, + "seriesOverrides": [] + } + ], + "axes": { + "xAxis": { + "displayName": "", + "visible": true + }, + "yAxes": [ + { + "displayName": "", + "visible": true, + "min": "AUTO", + "max": "AUTO", + "position": "LEFT", + "queryIds": [ + "A" + ], + "defaultAxis": true + } + ] + }, + "heatmapSettings": { + "yAxis": "VALUE" + }, + "thresholds": [ + { + "axisTarget": "LEFT", + "rules": [ + { + "value": 0, + "color": "#7dc540" + }, + { + "value": 68000, + "color": "#f5d30f" + }, + { + "value": 69000, + "color": "#dc172a" + } + ], + "queryId": "", + "visible": true + } + ], + "tableSettings": { + "isThresholdBackgroundAppliedToCell": false + }, + "graphChartSettings": { + "connectNulls": false + }, + "honeycombSettings": { + "showHive": true, + "showLegend": true, + "showLabels": false + } + }, + "queriesSettings": { + "resolution": "" + }, + "metricExpressions": [ + "resolution=null&(builtin:service.response.time:splitBy():avg:auto:sort(value(avg,descending)):limit(10)):limit(100):names" + ] + } + ] +}