Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: PC-13045 Good total single query experimental support #458

Merged
merged 13 commits into from
Jul 11, 2024
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ words:
- mockgen
- mprofile
- msgf
- mstats
- msteams
- ningxia
- nobl
Expand Down
11 changes: 10 additions & 1 deletion manifest/v1alpha/slo/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ type CountMetricsSpec struct {
Incremental *bool `json:"incremental"`
GoodMetric *MetricSpec `json:"good,omitempty"`
BadMetric *MetricSpec `json:"bad,omitempty"`
TotalMetric *MetricSpec `json:"total"`
TotalMetric *MetricSpec `json:"total,omitempty"`
mkaras-nobl9 marked this conversation as resolved.
Show resolved Hide resolved
// Experimental: Splunk only, a single query returning both good and total counts.
GoodTotalMetric *MetricSpec `json:"goodTotal,omitempty"`
}

// RawMetricSpec represents integration with a metric source for a particular objective.
Expand Down Expand Up @@ -143,6 +145,9 @@ func (s *Spec) CountMetricsCount() int {
if objective.CountMetrics.BadMetric != nil {
count++
}
if objective.CountMetrics.GoodTotalMetric != nil {
count++
}
}
}
return count
Expand All @@ -168,6 +173,10 @@ func (s *Spec) CountMetrics() []*MetricSpec {
countMetrics[i] = objective.CountMetrics.BadMetric
i++
}
if objective.CountMetrics.GoodTotalMetric != nil {
countMetrics[i] = objective.CountMetrics.GoodTotalMetric
i++
}
}
return countMetrics
}
Expand Down
12 changes: 12 additions & 0 deletions manifest/v1alpha/slo/metrics_bigquery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ func TestBigQuery_CountMetrics(t *testing.T) {
err := validate(slo)
testutils.AssertNoError(t, slo, err)
})
t.Run("unsupported goodTotal single query", func(t *testing.T) {
mkaras-nobl9 marked this conversation as resolved.
Show resolved Hide resolved
slo := validCountMetricSLO(v1alpha.BigQuery)
slo.Spec.Objectives[0].CountMetrics = &CountMetricsSpec{
Incremental: ptr(false),
GoodTotalMetric: validMetricSpec(v1alpha.BigQuery),
}
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics.goodTotal",
Code: joinErrorCodes(errCodeSingleQueryGoodOverTotalDisabled, validation.ErrorCodeOneOf),
})
})
t.Run("projectId must be the same for good and total", func(t *testing.T) {
slo := validCountMetricSLO(v1alpha.BigQuery)
slo.Spec.Objectives[0].CountMetrics.TotalMetric.BigQuery.ProjectID = "1"
Expand Down
33 changes: 33 additions & 0 deletions manifest/v1alpha/slo/metrics_splunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,33 @@ package slo
import (
"regexp"

"github.com/pkg/errors"

"github.com/nobl9/nobl9-go/internal/validation"
"github.com/nobl9/nobl9-go/manifest/v1alpha"
)

// SplunkMetric represents metric from Splunk
type SplunkMetric struct {
Query *string `json:"query"`
}

var splunkCountMetricsLevelValidation = validation.New[CountMetricsSpec](
validation.For(validation.GetSelf[CountMetricsSpec]()).
Rules(
validation.NewSingleRule(func(c CountMetricsSpec) error {
if c.GoodTotalMetric != nil {
if c.GoodMetric != nil || c.BadMetric != nil || c.TotalMetric != nil {
return errors.New("goodTotal is mutually exclusive with good, bad, and total")
}
}
return nil
}).WithErrorCode(validation.ErrorCodeMutuallyExclusive)),
).When(
whenCountMetricsIs(v1alpha.Splunk),
validation.WhenDescription("countMetrics is splunk"),
mkaras-nobl9 marked this conversation as resolved.
Show resolved Hide resolved
)

var splunkValidation = validation.New[SplunkMetric](
validation.ForPointer(func(s SplunkMetric) *string { return s.Query }).
WithName("query").
Expand All @@ -24,3 +43,17 @@ var splunkValidation = validation.New[SplunkMetric](
"index=svc-events", `"index"=svc-events`).
WithDetails(`query has to contain index=<NAME> or "index"=<NAME>`)),
)

var splunkSingleQueryValidation = validation.New[SplunkMetric](
validation.ForPointer(func(s SplunkMetric) *string { return s.Query }).
WithName("query").
Required().
Cascade(validation.CascadeModeStop).
Rules(validation.StringNotEmpty()).
Rules(
validation.StringContains("n9time", "n9good", "n9total"),
validation.StringMatchRegexp(
regexp.MustCompile(`(\bindex\s*=.+)|("\bindex"\s*=.+)`),
"index=svc-events", `"index"=svc-events`).
WithDetails(`query has to contain index=<NAME> or "index"=<NAME>`)),
)
113 changes: 113 additions & 0 deletions manifest/v1alpha/slo/metrics_splunk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,116 @@ fields n9time n9value`,
}
})
}

func TestSplunk_CountMetrics_SingleQuery(t *testing.T) {
t.Run("passes", func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
err := validate(slo)
testutils.AssertNoError(t, slo, err)
})
t.Run("required", func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
slo.Spec.Objectives[0].CountMetrics.GoodTotalMetric.Splunk.Query = nil
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics.goodTotal.splunk.query",
Code: validation.ErrorCodeRequired,
})
})
t.Run("empty", func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
slo.Spec.Objectives[0].CountMetrics.GoodTotalMetric.Splunk.Query = ptr("")
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics.goodTotal.splunk.query",
Code: validation.ErrorCodeStringNotEmpty,
})
})
t.Run("goodTotal mixed with total", func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
slo.Spec.Objectives[0].CountMetrics.TotalMetric = validMetricSpec(v1alpha.Splunk)
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics",
Code: validation.ErrorCodeMutuallyExclusive,
})
})
t.Run("goodTotal mixed with good", func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
slo.Spec.Objectives[0].CountMetrics.GoodMetric = validMetricSpec(v1alpha.Splunk)
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics",
Code: validation.ErrorCodeMutuallyExclusive,
})
})
t.Run("goodTotal mixed with bad", func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
slo.Spec.Objectives[0].CountMetrics.BadMetric = validMetricSpec(v1alpha.Splunk)
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 2, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics.bad",
Code: joinErrorCodes(errCodeBadOverTotalDisabled, validation.ErrorCodeOneOf),
}, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics",
Code: validation.ErrorCodeMutuallyExclusive,
})
})
t.Run("invalid query", func(t *testing.T) {
tests := map[string]struct {
Query string
ExpectedCode string
}{
"missing n9time": {
Query: `
| mstats avg("spl.intr.resource_usage.IOWait.data.avg_cpu_pct") as n9good WHERE index="_metrics" span=15s
| join type=left _time [
| mstats avg("spl.intr.resource_usage.IOWait.data.max_cpus_pct") as n9total WHERE index="_metrics" span=15s
]
| fields _time n9good n9total`,
ExpectedCode: validation.ErrorCodeStringContains,
},
"missing n9good": {
Query: `
| mstats avg("spl.intr.resource_usage.IOWait.data.avg_cpu_pct") as good WHERE index="_metrics" span=15s
| join type=left _time [
| mstats avg("spl.intr.resource_usage.IOWait.data.max_cpus_pct") as n9total WHERE index="_metrics" span=15s
]
| rename _time as n9time
| fields n9time good n9total`,
ExpectedCode: validation.ErrorCodeStringContains,
},
"missing n9total": {
Query: `
| mstats avg("spl.intr.resource_usage.IOWait.data.avg_cpu_pct") as n9good WHERE index="_metrics" span=15s
| join type=left _time [
| mstats avg("spl.intr.resource_usage.IOWait.data.max_cpus_pct") as total WHERE index="_metrics" span=15s
]
| rename _time as n9time
| fields n9time n9good total`,
ExpectedCode: validation.ErrorCodeStringContains,
},
"missing index": {
Query: `
| mstats avg("spl.intr.resource_usage.IOWait.data.avg_cpu_pct") as n9good span=15s
| join type=left _time [
| mstats avg("spl.intr.resource_usage.IOWait.data.max_cpus_pct") as n9total span=15s
]
| rename _time as n9time
| fields n9time n9good n9total`,
ExpectedCode: validation.ErrorCodeStringMatchRegexp,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
slo := validSingleQueryGoodOverTotalCountMetricSLO(v1alpha.Splunk)
slo.Spec.Objectives[0].CountMetrics.GoodTotalMetric.Splunk.Query = ptr(test.Query)
err := validate(slo)
testutils.AssertContainsErrors(t, slo, err, 1, testutils.ExpectedError{
Prop: "spec.objectives[0].countMetrics.goodTotal.splunk.query",
Code: test.ExpectedCode,
})
})
}
})
}
44 changes: 37 additions & 7 deletions manifest/v1alpha/slo/metrics_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
)

const (
errCodeExactlyOneMetricType = "exactly_one_metric_type"
errCodeBadOverTotalDisabled = "bad_over_total_disabled"
errCodeExactlyOneMetricSpecType = "exactly_one_metric_spec_type"
errCodeEitherBadOrGoodCountMetric = "either_bad_or_good_count_metric"
errCodeTimeSliceTarget = "time_slice_target"
errCodeExactlyOneMetricType = "exactly_one_metric_type"
errCodeBadOverTotalDisabled = "bad_over_total_disabled"
errCodeSingleQueryGoodOverTotalDisabled = "single_query_good_over_total_disabled"
errCodeExactlyOneMetricSpecType = "exactly_one_metric_spec_type"
errCodeEitherBadOrGoodCountMetric = "either_bad_or_good_count_metric"
errCodeTimeSliceTarget = "time_slice_target"
)

var specMetricsValidation = validation.New[Spec](
Expand Down Expand Up @@ -61,13 +62,13 @@ var countMetricsSpecValidation = validation.New[CountMetricsSpec](
sumoLogicCountMetricsLevelValidation,
instanaCountMetricsLevelValidation,
redshiftCountMetricsLevelValidation,
bigQueryCountMetricsLevelValidation),
bigQueryCountMetricsLevelValidation,
splunkCountMetricsLevelValidation),
validation.ForPointer(func(c CountMetricsSpec) *bool { return c.Incremental }).
WithName("incremental").
Required(),
validation.ForPointer(func(c CountMetricsSpec) *MetricSpec { return c.TotalMetric }).
WithName("total").
Required().
Include(
metricSpecValidation,
countMetricsValidation,
Expand All @@ -84,6 +85,12 @@ var countMetricsSpecValidation = validation.New[CountMetricsSpec](
Include(
countMetricsValidation,
metricSpecValidation),
validation.ForPointer(func(c CountMetricsSpec) *MetricSpec { return c.GoodTotalMetric }).
WithName("goodTotal").
Rules(oneOfSingleQueryGoodOverTotalValidationRule).
Include(
countMetricsValidation,
singleQueryMetricSpecValidation),
)

var rawMetricsValidation = validation.New[RawMetricSpec](
Expand All @@ -106,6 +113,12 @@ var countMetricsValidation = validation.New[MetricSpec](
instanaCountMetricsValidation),
)

var singleQueryMetricSpecValidation = validation.New[MetricSpec](
validation.ForPointer(func(m MetricSpec) *SplunkMetric { return m.Splunk }).
WithName("splunk").
Include(splunkSingleQueryValidation),
)

var metricSpecValidation = validation.New[MetricSpec](
validation.ForPointer(func(m MetricSpec) *AppDynamicsMetric { return m.AppDynamics }).
WithName("appDynamics").
Expand Down Expand Up @@ -199,6 +212,17 @@ var oneOfBadOverTotalValidationRule = validation.NewSingleRule(func(v MetricSpec
return validation.OneOf(badOverTotalEnabledSources...).Validate(v.DataSourceType())
}).WithErrorCode(errCodeBadOverTotalDisabled)

var singleQueryGoodOverTotalEnabledSources = []v1alpha.DataSourceType{
v1alpha.Splunk,
}

// Support for single query good/total metrics is experimental.
// Splunk is the only datasource integration to have this feature
// - extend the list while adding support for next integrations.
var oneOfSingleQueryGoodOverTotalValidationRule = validation.NewSingleRule(func(v MetricSpec) error {
return validation.OneOf(singleQueryGoodOverTotalEnabledSources...).Validate(v.DataSourceType())
}).WithErrorCode(errCodeSingleQueryGoodOverTotalDisabled)

var exactlyOneMetricSpecTypeValidationRule = validation.NewSingleRule(func(v Spec) error {
if v.Indicator == nil {
return nil
Expand Down Expand Up @@ -400,6 +424,12 @@ var timeSliceTargetsValidationRule = validation.NewSingleRule[Spec](func(s Spec)
// the count metrics is of the given type.
func whenCountMetricsIs(typ v1alpha.DataSourceType) func(c CountMetricsSpec) bool {
mkaras-nobl9 marked this conversation as resolved.
Show resolved Hide resolved
return func(c CountMetricsSpec) bool {
if slices.Contains(singleQueryGoodOverTotalEnabledSources, typ) {
if c.GoodTotalMetric != nil && typ != c.GoodTotalMetric.DataSourceType() {
return false
}
return c.GoodMetric != nil || c.BadMetric != nil || c.TotalMetric != nil
}
if c.TotalMetric == nil {
return false
}
Expand Down
34 changes: 34 additions & 0 deletions manifest/v1alpha/slo/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"golang.org/x/exp/slices"

"github.com/nobl9/nobl9-go/internal/manifest/v1alphatest"
"github.com/nobl9/nobl9-go/internal/testutils"
Expand Down Expand Up @@ -1326,6 +1327,19 @@ func validCountMetricSLO(metricType v1alpha.DataSourceType) SLO {
return s
}

// nolint:unparam
func validSingleQueryGoodOverTotalCountMetricSLO(metricType v1alpha.DataSourceType) SLO {
s := validSLO()
if !slices.Contains(singleQueryGoodOverTotalEnabledSources, metricType) {
panic("metric type not supported")
}
s.Spec.Objectives[0].CountMetrics = &CountMetricsSpec{
Incremental: ptr(false),
GoodTotalMetric: validSingleQueryMetricSpec(metricType),
}
return s
}

func validSLO() SLO {
return New(
Metadata{
Expand Down Expand Up @@ -1682,6 +1696,26 @@ fetch consumed_api
}},
}

func validSingleQueryMetricSpec(typ v1alpha.DataSourceType) *MetricSpec {
ms := validSingleQueryMetricSpecs[typ]
var clone MetricSpec
data, _ := json.Marshal(ms)
_ = json.Unmarshal(data, &clone)
return &clone
}

var validSingleQueryMetricSpecs = map[v1alpha.DataSourceType]MetricSpec{
v1alpha.Splunk: {Splunk: &SplunkMetric{
Query: ptr(`
| mstats avg("spl.intr.resource_usage.IOWait.data.avg_cpu_pct") as n9good WHERE index="_metrics" span=15s
| join type=left _time [
| mstats avg("spl.intr.resource_usage.IOWait.data.max_cpus_pct") as n9total WHERE index="_metrics" span=15s
]
| rename _time as n9time
| fields n9time n9good n9total`),
}},
}

func ptr[T any](v T) *T { return &v }

func joinErrorCodes(codes ...string) string {
Expand Down
Loading