Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions api/datadoghq/v1alpha1/datadogslo_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type DatadogSLOSpec struct {
// Note that only the `sum by` aggregator is allowed, which sums all request counts. `Average`, `max`, nor `min` request aggregators are not supported.
Query *DatadogSLOQuery `json:"query,omitempty"`

// TimeSlice defines the SLI specification for a time_slice SLO. Required if type is time_slice.
// It specifies a metric query and a comparator/threshold that determines what counts as good uptime.
TimeSlice *DatadogSLOTimeSlice `json:"timeSlice,omitempty"`

// Type is the type of the service level objective.
Type DatadogSLOType `json:"type"`

Expand All @@ -63,16 +67,45 @@ type DatadogSLOQuery struct {
Denominator string `json:"denominator"`
}

// DatadogSLOTimeSlice defines the SLI specification for a time_slice SLO.
// It specifies a metric query and a comparator/threshold that determines what counts as good uptime.
// The operator automatically wraps the query into the formula and named query structure required by the Datadog API.
// +k8s:openapi-gen=true
type DatadogSLOTimeSlice struct {
// Query is a Datadog metric query string that produces the SLI value.
Query string `json:"query"`

// Comparator is the comparison operator used to compare the SLI value to the threshold.
// +kubebuilder:validation:Enum=">";">=";"<";"<="
Comparator DatadogSLOTimeSliceComparator `json:"comparator"`

// Threshold is the value against which the SLI is compared using the comparator to determine
// if a time slice is good or bad.
Threshold resource.Quantity `json:"threshold"`
}

// DatadogSLOTimeSliceComparator is the comparator used to compare the SLI value to the threshold.
// +kubebuilder:validation:Enum=">";">=";"<";"<="
type DatadogSLOTimeSliceComparator string

const (
DatadogSLOTimeSliceComparatorGreater DatadogSLOTimeSliceComparator = ">"
DatadogSLOTimeSliceComparatorGreaterEqual DatadogSLOTimeSliceComparator = ">="
DatadogSLOTimeSliceComparatorLess DatadogSLOTimeSliceComparator = "<"
DatadogSLOTimeSliceComparatorLessEqual DatadogSLOTimeSliceComparator = "<="
)

type DatadogSLOType string

const (
DatadogSLOTypeMetric DatadogSLOType = "metric"
DatadogSLOTypeMonitor DatadogSLOType = "monitor"
DatadogSLOTypeMetric DatadogSLOType = "metric"
DatadogSLOTypeMonitor DatadogSLOType = "monitor"
DatadogSLOTypeTimeSlice DatadogSLOType = "time_slice"
)

func (t DatadogSLOType) IsValid() bool {
switch t {
case DatadogSLOTypeMetric, DatadogSLOTypeMonitor:
case DatadogSLOTypeMetric, DatadogSLOTypeMonitor, DatadogSLOTypeTimeSlice:
return true
default:
return false
Expand Down
26 changes: 25 additions & 1 deletion api/datadoghq/v1alpha1/datadogslo_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func IsValidDatadogSLO(spec *DatadogSLOSpec) error {
}

if spec.Type != "" && !spec.Type.IsValid() {
errs = append(errs, fmt.Errorf("spec.Type must be one of the values: %s or %s", DatadogSLOTypeMonitor, DatadogSLOTypeMetric))
errs = append(errs, fmt.Errorf("spec.Type must be one of the values: %s, %s, or %s", DatadogSLOTypeMonitor, DatadogSLOTypeMetric, DatadogSLOTypeTimeSlice))
}

if spec.Type == DatadogSLOTypeMetric && spec.Query == nil {
Expand All @@ -35,6 +35,30 @@ func IsValidDatadogSLO(spec *DatadogSLOSpec) error {
errs = append(errs, fmt.Errorf("spec.MonitorIDs must be defined when spec.Type is monitor"))
}

if spec.Type == DatadogSLOTypeTimeSlice {
if spec.TimeSlice == nil {
errs = append(errs, fmt.Errorf("spec.TimeSlice must be defined when spec.Type is time_slice"))
} else {
if spec.TimeSlice.Query == "" {
errs = append(errs, fmt.Errorf("spec.TimeSlice.Query must be defined"))
}
}
}

// Cross-field validation: reject fields that don't belong to the specified type.
if spec.Type == DatadogSLOTypeMetric && spec.TimeSlice != nil {
errs = append(errs, fmt.Errorf("spec.TimeSlice must not be defined when spec.Type is metric"))
}
if spec.Type == DatadogSLOTypeMonitor && spec.TimeSlice != nil {
errs = append(errs, fmt.Errorf("spec.TimeSlice must not be defined when spec.Type is monitor"))
}
if spec.Type == DatadogSLOTypeTimeSlice && spec.Query != nil {
errs = append(errs, fmt.Errorf("spec.Query must not be defined when spec.Type is time_slice"))
}
if spec.Type == DatadogSLOTypeTimeSlice && len(spec.MonitorIDs) > 0 {
errs = append(errs, fmt.Errorf("spec.MonitorIDs must not be defined when spec.Type is time_slice"))
}

if spec.TargetThreshold.AsApproximateFloat64() <= 0 || spec.TargetThreshold.AsApproximateFloat64() >= 100 {
errs = append(errs, fmt.Errorf("spec.TargetThreshold must be greater than 0 and less than 100"))
}
Expand Down
112 changes: 111 additions & 1 deletion api/datadoghq/v1alpha1/datadogslo_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestIsValidDatadogSLO(t *testing.T) {
TargetThreshold: resource.MustParse("99.99"),
Timeframe: DatadogSLOTimeFrame30d,
},
expected: errors.New("spec.Type must be one of the values: monitor or metric"),
expected: errors.New("spec.Type must be one of the values: monitor, metric, or time_slice"),
},
{
name: "Missing Threshold and Timeframe",
Expand Down Expand Up @@ -157,6 +157,116 @@ func TestIsValidDatadogSLO(t *testing.T) {
},
expected: errors.New("spec.Timeframe must be defined as one of the values: 7d, 30d, or 90d"),
},
{
name: "Valid time_slice spec",
spec: &DatadogSLOSpec{
Name: "TimeSliceSLO",
Type: DatadogSLOTypeTimeSlice,
TimeSlice: &DatadogSLOTimeSlice{
Query: "trace.servlet.request{env:prod}",
Comparator: DatadogSLOTimeSliceComparatorGreater,
Threshold: resource.MustParse("5"),
},
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: nil,
},
{
name: "Missing TimeSlice when type is time_slice",
spec: &DatadogSLOSpec{
Name: "TimeSliceSLO",
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSlice must be defined when spec.Type is time_slice"),
},
{
name: "Empty query in time_slice",
spec: &DatadogSLOSpec{
Name: "TimeSliceSLO",
Type: DatadogSLOTypeTimeSlice,
TimeSlice: &DatadogSLOTimeSlice{
Query: "",
Comparator: DatadogSLOTimeSliceComparatorGreater,
Threshold: resource.MustParse("5"),
},
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSlice.Query must be defined"),
},
{
name: "time_slice type with Query set is invalid",
spec: &DatadogSLOSpec{
Name: "TimeSliceSLO",
Type: DatadogSLOTypeTimeSlice,
TimeSlice: &DatadogSLOTimeSlice{
Query: "trace.servlet.request{env:prod}",
Comparator: DatadogSLOTimeSliceComparatorGreater,
Threshold: resource.MustParse("5"),
},
Query: &DatadogSLOQuery{
Numerator: "good",
Denominator: "total",
},
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.Query must not be defined when spec.Type is time_slice"),
},
{
name: "time_slice type with MonitorIDs set is invalid",
spec: &DatadogSLOSpec{
Name: "TimeSliceSLO",
Type: DatadogSLOTypeTimeSlice,
TimeSlice: &DatadogSLOTimeSlice{
Query: "trace.servlet.request{env:prod}",
Comparator: DatadogSLOTimeSliceComparatorGreater,
Threshold: resource.MustParse("5"),
},
MonitorIDs: []int64{12345},
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.MonitorIDs must not be defined when spec.Type is time_slice"),
},
{
name: "metric type with TimeSlice set is invalid",
spec: &DatadogSLOSpec{
Name: "MySLO",
Type: DatadogSLOTypeMetric,
Query: &DatadogSLOQuery{
Numerator: "good",
Denominator: "total",
},
TimeSlice: &DatadogSLOTimeSlice{
Query: "trace.servlet.request{env:prod}",
Comparator: DatadogSLOTimeSliceComparatorGreater,
Threshold: resource.MustParse("5"),
},
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSlice must not be defined when spec.Type is metric"),
},
{
name: "monitor type with TimeSlice set is invalid",
spec: &DatadogSLOSpec{
Name: "MySLO",
Type: DatadogSLOTypeMonitor,
MonitorIDs: []int64{12345},
TimeSlice: &DatadogSLOTimeSlice{
Query: "trace.servlet.request{env:prod}",
Comparator: DatadogSLOTimeSliceComparatorGreater,
Threshold: resource.MustParse("5"),
},
TargetThreshold: resource.MustParse("97"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSlice must not be defined when spec.Type is monitor"),
},
}

for _, tt := range tests {
Expand Down
21 changes: 21 additions & 0 deletions api/datadoghq/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 46 additions & 1 deletion api/datadoghq/v1alpha1/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions config/crd/bases/v1/datadoghq.com_datadogslos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ spec:
description: TargetThreshold is the target threshold such that when the service level indicator is above this threshold over the given timeframe, the objective is being met.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
timeSlice:
description: |-
TimeSlice defines the SLI specification for a time_slice SLO. Required if type is time_slice.
It specifies a metric query and a comparator/threshold that determines what counts as good uptime.
properties:
comparator:
allOf:
- enum:
- '>'
- '>='
- <
- <=
- enum:
- '>'
- '>='
- <
- <=
description: Comparator is the comparison operator used to compare the SLI value to the threshold.
type: string
query:
description: Query is a Datadog metric query string that produces the SLI value.
type: string
threshold:
anyOf:
- type: integer
- type: string
description: |-
Threshold is the value against which the SLI is compared using the comparator to determine
if a time slice is good or bad.
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
required:
- comparator
- query
- threshold
type: object
timeframe:
description: The SLO time window options.
type: string
Expand Down
Loading
Loading