Skip to content

Commit 62fdb06

Browse files
levan-mcedi
andauthored
Add time_slice SLO type support (#2875)
Add support for Datadog Time Slice SLOs alongside the existing metric and monitor SLO types. Time Slice SLOs use an SLI specification with metric queries, formulas, a comparator, and a threshold to determine what counts as good uptime in each time slice. - Add DatadogSLOTypeTimeSlice and related CRD types (DatadogSLOTimeSlice, DatadogSLOFormula, DatadogSLODataSourceQuery, DatadogSLOTimeSliceComparator) - Add TimeSlice field to DatadogSLOSpec following the existing pattern where each SLO type has its own dedicated payload field - Add validation for time_slice specs including cross-field rejection to prevent invalid type/field combinations - Add buildSliSpecification() mapping to the datadogV1 SDK types with hardcoded metrics data source - Add comprehensive unit tests for validation and API mapping - Regenerate CRDs and deepcopy/openapi code Co-authored-by: Cedric Specht <cedric@specht-labs.de>
1 parent a0dc8c0 commit 62fdb06

9 files changed

Lines changed: 548 additions & 6 deletions

File tree

api/datadoghq/v1alpha1/datadogslo_types.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ type DatadogSLOSpec struct {
3939
// Note that only the `sum by` aggregator is allowed, which sums all request counts. `Average`, `max`, nor `min` request aggregators are not supported.
4040
Query *DatadogSLOQuery `json:"query,omitempty"`
4141

42+
// TimeSlice defines the SLI specification for a time_slice SLO. Required if type is time_slice.
43+
// It specifies a metric query and a comparator/threshold that determines what counts as good uptime.
44+
TimeSlice *DatadogSLOTimeSlice `json:"timeSlice,omitempty"`
45+
4246
// Type is the type of the service level objective.
4347
Type DatadogSLOType `json:"type"`
4448

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

70+
// DatadogSLOTimeSlice defines the SLI specification for a time_slice SLO.
71+
// It specifies a metric query and a comparator/threshold that determines what counts as good uptime.
72+
// The operator automatically wraps the query into the formula and named query structure required by the Datadog API.
73+
// +k8s:openapi-gen=true
74+
type DatadogSLOTimeSlice struct {
75+
// Query is a Datadog metric query string that produces the SLI value.
76+
Query string `json:"query"`
77+
78+
// Comparator is the comparison operator used to compare the SLI value to the threshold.
79+
// +kubebuilder:validation:Enum=">";">=";"<";"<="
80+
Comparator DatadogSLOTimeSliceComparator `json:"comparator"`
81+
82+
// Threshold is the value against which the SLI is compared using the comparator to determine
83+
// if a time slice is good or bad.
84+
Threshold resource.Quantity `json:"threshold"`
85+
}
86+
87+
// DatadogSLOTimeSliceComparator is the comparator used to compare the SLI value to the threshold.
88+
// +kubebuilder:validation:Enum=">";">=";"<";"<="
89+
type DatadogSLOTimeSliceComparator string
90+
91+
const (
92+
DatadogSLOTimeSliceComparatorGreater DatadogSLOTimeSliceComparator = ">"
93+
DatadogSLOTimeSliceComparatorGreaterEqual DatadogSLOTimeSliceComparator = ">="
94+
DatadogSLOTimeSliceComparatorLess DatadogSLOTimeSliceComparator = "<"
95+
DatadogSLOTimeSliceComparatorLessEqual DatadogSLOTimeSliceComparator = "<="
96+
)
97+
6698
type DatadogSLOType string
6799

68100
const (
69-
DatadogSLOTypeMetric DatadogSLOType = "metric"
70-
DatadogSLOTypeMonitor DatadogSLOType = "monitor"
101+
DatadogSLOTypeMetric DatadogSLOType = "metric"
102+
DatadogSLOTypeMonitor DatadogSLOType = "monitor"
103+
DatadogSLOTypeTimeSlice DatadogSLOType = "time_slice"
71104
)
72105

73106
func (t DatadogSLOType) IsValid() bool {
74107
switch t {
75-
case DatadogSLOTypeMetric, DatadogSLOTypeMonitor:
108+
case DatadogSLOTypeMetric, DatadogSLOTypeMonitor, DatadogSLOTypeTimeSlice:
76109
return true
77110
default:
78111
return false

api/datadoghq/v1alpha1/datadogslo_validation.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func IsValidDatadogSLO(spec *DatadogSLOSpec) error {
2424
}
2525

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

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

38+
if spec.Type == DatadogSLOTypeTimeSlice {
39+
if spec.TimeSlice == nil {
40+
errs = append(errs, fmt.Errorf("spec.TimeSlice must be defined when spec.Type is time_slice"))
41+
} else {
42+
if spec.TimeSlice.Query == "" {
43+
errs = append(errs, fmt.Errorf("spec.TimeSlice.Query must be defined"))
44+
}
45+
}
46+
}
47+
48+
// Cross-field validation: reject fields that don't belong to the specified type.
49+
if spec.Type == DatadogSLOTypeMetric && spec.TimeSlice != nil {
50+
errs = append(errs, fmt.Errorf("spec.TimeSlice must not be defined when spec.Type is metric"))
51+
}
52+
if spec.Type == DatadogSLOTypeMonitor && spec.TimeSlice != nil {
53+
errs = append(errs, fmt.Errorf("spec.TimeSlice must not be defined when spec.Type is monitor"))
54+
}
55+
if spec.Type == DatadogSLOTypeTimeSlice && spec.Query != nil {
56+
errs = append(errs, fmt.Errorf("spec.Query must not be defined when spec.Type is time_slice"))
57+
}
58+
if spec.Type == DatadogSLOTypeTimeSlice && len(spec.MonitorIDs) > 0 {
59+
errs = append(errs, fmt.Errorf("spec.MonitorIDs must not be defined when spec.Type is time_slice"))
60+
}
61+
3862
if spec.TargetThreshold.AsApproximateFloat64() <= 0 || spec.TargetThreshold.AsApproximateFloat64() >= 100 {
3963
errs = append(errs, fmt.Errorf("spec.TargetThreshold must be greater than 0 and less than 100"))
4064
}

api/datadoghq/v1alpha1/datadogslo_validation_test.go

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func TestIsValidDatadogSLO(t *testing.T) {
8484
TargetThreshold: resource.MustParse("99.99"),
8585
Timeframe: DatadogSLOTimeFrame30d,
8686
},
87-
expected: errors.New("spec.Type must be one of the values: monitor or metric"),
87+
expected: errors.New("spec.Type must be one of the values: monitor, metric, or time_slice"),
8888
},
8989
{
9090
name: "Missing Threshold and Timeframe",
@@ -158,6 +158,116 @@ func TestIsValidDatadogSLO(t *testing.T) {
158158
},
159159
expected: errors.New("spec.Timeframe must be defined as one of the values: 7d, 30d, or 90d"),
160160
},
161+
{
162+
name: "Valid time_slice spec",
163+
spec: &DatadogSLOSpec{
164+
Name: "TimeSliceSLO",
165+
Type: DatadogSLOTypeTimeSlice,
166+
TimeSlice: &DatadogSLOTimeSlice{
167+
Query: "trace.servlet.request{env:prod}",
168+
Comparator: DatadogSLOTimeSliceComparatorGreater,
169+
Threshold: resource.MustParse("5"),
170+
},
171+
TargetThreshold: resource.MustParse("97"),
172+
Timeframe: DatadogSLOTimeFrame7d,
173+
},
174+
expected: nil,
175+
},
176+
{
177+
name: "Missing TimeSlice when type is time_slice",
178+
spec: &DatadogSLOSpec{
179+
Name: "TimeSliceSLO",
180+
Type: DatadogSLOTypeTimeSlice,
181+
TargetThreshold: resource.MustParse("97"),
182+
Timeframe: DatadogSLOTimeFrame7d,
183+
},
184+
expected: errors.New("spec.TimeSlice must be defined when spec.Type is time_slice"),
185+
},
186+
{
187+
name: "Empty query in time_slice",
188+
spec: &DatadogSLOSpec{
189+
Name: "TimeSliceSLO",
190+
Type: DatadogSLOTypeTimeSlice,
191+
TimeSlice: &DatadogSLOTimeSlice{
192+
Query: "",
193+
Comparator: DatadogSLOTimeSliceComparatorGreater,
194+
Threshold: resource.MustParse("5"),
195+
},
196+
TargetThreshold: resource.MustParse("97"),
197+
Timeframe: DatadogSLOTimeFrame7d,
198+
},
199+
expected: errors.New("spec.TimeSlice.Query must be defined"),
200+
},
201+
{
202+
name: "time_slice type with Query set is invalid",
203+
spec: &DatadogSLOSpec{
204+
Name: "TimeSliceSLO",
205+
Type: DatadogSLOTypeTimeSlice,
206+
TimeSlice: &DatadogSLOTimeSlice{
207+
Query: "trace.servlet.request{env:prod}",
208+
Comparator: DatadogSLOTimeSliceComparatorGreater,
209+
Threshold: resource.MustParse("5"),
210+
},
211+
Query: &DatadogSLOQuery{
212+
Numerator: "good",
213+
Denominator: "total",
214+
},
215+
TargetThreshold: resource.MustParse("97"),
216+
Timeframe: DatadogSLOTimeFrame7d,
217+
},
218+
expected: errors.New("spec.Query must not be defined when spec.Type is time_slice"),
219+
},
220+
{
221+
name: "time_slice type with MonitorIDs set is invalid",
222+
spec: &DatadogSLOSpec{
223+
Name: "TimeSliceSLO",
224+
Type: DatadogSLOTypeTimeSlice,
225+
TimeSlice: &DatadogSLOTimeSlice{
226+
Query: "trace.servlet.request{env:prod}",
227+
Comparator: DatadogSLOTimeSliceComparatorGreater,
228+
Threshold: resource.MustParse("5"),
229+
},
230+
MonitorIDs: []int64{12345},
231+
TargetThreshold: resource.MustParse("97"),
232+
Timeframe: DatadogSLOTimeFrame7d,
233+
},
234+
expected: errors.New("spec.MonitorIDs must not be defined when spec.Type is time_slice"),
235+
},
236+
{
237+
name: "metric type with TimeSlice set is invalid",
238+
spec: &DatadogSLOSpec{
239+
Name: "MySLO",
240+
Type: DatadogSLOTypeMetric,
241+
Query: &DatadogSLOQuery{
242+
Numerator: "good",
243+
Denominator: "total",
244+
},
245+
TimeSlice: &DatadogSLOTimeSlice{
246+
Query: "trace.servlet.request{env:prod}",
247+
Comparator: DatadogSLOTimeSliceComparatorGreater,
248+
Threshold: resource.MustParse("5"),
249+
},
250+
TargetThreshold: resource.MustParse("97"),
251+
Timeframe: DatadogSLOTimeFrame7d,
252+
},
253+
expected: errors.New("spec.TimeSlice must not be defined when spec.Type is metric"),
254+
},
255+
{
256+
name: "monitor type with TimeSlice set is invalid",
257+
spec: &DatadogSLOSpec{
258+
Name: "MySLO",
259+
Type: DatadogSLOTypeMonitor,
260+
MonitorIDs: []int64{12345},
261+
TimeSlice: &DatadogSLOTimeSlice{
262+
Query: "trace.servlet.request{env:prod}",
263+
Comparator: DatadogSLOTimeSliceComparatorGreater,
264+
Threshold: resource.MustParse("5"),
265+
},
266+
TargetThreshold: resource.MustParse("97"),
267+
Timeframe: DatadogSLOTimeFrame7d,
268+
},
269+
expected: errors.New("spec.TimeSlice must not be defined when spec.Type is monitor"),
270+
},
161271
}
162272

163273
for _, tt := range tests {

api/datadoghq/v1alpha1/zz_generated.deepcopy.go

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/datadoghq/v1alpha1/zz_generated.openapi.go

Lines changed: 46 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/v1/datadoghq.com_datadogslos.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,42 @@ spec:
112112
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.
113113
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
114114
x-kubernetes-int-or-string: true
115+
timeSlice:
116+
description: |-
117+
TimeSlice defines the SLI specification for a time_slice SLO. Required if type is time_slice.
118+
It specifies a metric query and a comparator/threshold that determines what counts as good uptime.
119+
properties:
120+
comparator:
121+
allOf:
122+
- enum:
123+
- '>'
124+
- '>='
125+
- <
126+
- <=
127+
- enum:
128+
- '>'
129+
- '>='
130+
- <
131+
- <=
132+
description: Comparator is the comparison operator used to compare the SLI value to the threshold.
133+
type: string
134+
query:
135+
description: Query is a Datadog metric query string that produces the SLI value.
136+
type: string
137+
threshold:
138+
anyOf:
139+
- type: integer
140+
- type: string
141+
description: |-
142+
Threshold is the value against which the SLI is compared using the comparator to determine
143+
if a time slice is good or bad.
144+
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
145+
x-kubernetes-int-or-string: true
146+
required:
147+
- comparator
148+
- query
149+
- threshold
150+
type: object
115151
timeframe:
116152
description: The SLO time window options.
117153
type: string

0 commit comments

Comments
 (0)