Skip to content
Draft
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
50 changes: 47 additions & 3 deletions api/datadoghq/v1alpha1/datadogslo_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ 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"`

// TimeSliceSpec is the specification for a time-slice SLO. Required if type is time_slice.
TimeSliceSpec *DatadogSLOTimeSliceSpec `json:"timeSliceSpec,omitempty"`

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

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

// +k8s:openapi-gen=true
type DatadogSLOTimeSliceSpec struct {
// TimeSliceCondition is the condition for the time-slice SLO.
TimeSliceCondition DatadogSLOTimeSliceCondition `json:"timeSliceCondition"`
// Query is the query for the time-slice SLO.
Query DatadogSLOTimeSliceQuery `json:"query"`
}

// +k8s:openapi-gen=true
type DatadogSLOTimeSliceCondition struct {
// Comparator is the comparator used for the time-slice condition.
Comparator string `json:"comparator"`
// Threshold is the threshold value for the time-slice condition.
Threshold resource.Quantity `json:"threshold"`
}

// +k8s:openapi-gen=true
type DatadogSLOTimeSliceQuery struct {
// Formulas is a list of formulas for the time-slice SLO query.
Formulas []DatadogSLOFormula `json:"formulas"`
// Queries is a list of queries for the time-slice SLO query.
Queries []DatadogSLOQueryDefinition `json:"queries"`
}

// +k8s:openapi-gen=true
type DatadogSLOFormula struct {
// Formula is the formula string.
Formula string `json:"formula"`
}

// +k8s:openapi-gen=true
type DatadogSLOQueryDefinition struct {
// DataSource is the data source for the query.
DataSource string `json:"dataSource"`
// Name is the name of the query.
Name string `json:"name"`
// Query is the query string.
Query string `json:"query"`
}

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
45 changes: 44 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,49 @@ 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.TimeSliceSpec == nil {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec must be defined when spec.Type is time_slice"))
} else {
// Validate time-slice specific fields
if spec.TimeSliceSpec.TimeSliceCondition.Comparator == "" {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.TimeSliceCondition.Comparator must be defined"))
}

if spec.TimeSliceSpec.TimeSliceCondition.Threshold.AsApproximateFloat64() == 0 {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.TimeSliceCondition.Threshold must be defined and greater than 0"))
}

if len(spec.TimeSliceSpec.Query.Formulas) == 0 {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.Query.Formulas must contain at least one formula"))
}

if len(spec.TimeSliceSpec.Query.Queries) == 0 {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.Query.Queries must contain at least one query"))
}

// Validate each formula
for i, formula := range spec.TimeSliceSpec.Query.Formulas {
if formula.Formula == "" {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.Query.Formulas[%d].Formula must not be empty", i))
}
}

// Validate each query
for i, query := range spec.TimeSliceSpec.Query.Queries {
if query.DataSource == "" {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.Query.Queries[%d].DataSource must be defined", i))
}
if query.Name == "" {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.Query.Queries[%d].Name must be defined", i))
}
if query.Query == "" {
errs = append(errs, fmt.Errorf("spec.TimeSliceSpec.Query.Queries[%d].Query must be defined", i))
}
}
}
}

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
153 changes: 153 additions & 0 deletions api/datadoghq/v1alpha1/datadogslo_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,159 @@ 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: "MyTimeSliceSLO",
TimeSliceSpec: &DatadogSLOTimeSliceSpec{
TimeSliceCondition: DatadogSLOTimeSliceCondition{
Comparator: ">",
Threshold: resource.MustParse("5.0"),
},
Query: DatadogSLOTimeSliceQuery{
Formulas: []DatadogSLOFormula{
{Formula: "query1"},
},
Queries: []DatadogSLOQueryDefinition{
{
DataSource: "metrics",
Name: "query1",
Query: "avg:system.cpu.user{*}",
},
},
},
},
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("99.0"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: nil,
},
{
name: "Missing TimeSliceSpec for time-slice type",
spec: &DatadogSLOSpec{
Name: "MyTimeSliceSLO",
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("99.0"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSliceSpec must be defined when spec.Type is time_slice"),
},
{
name: "Missing comparator in time-slice condition",
spec: &DatadogSLOSpec{
Name: "MyTimeSliceSLO",
TimeSliceSpec: &DatadogSLOTimeSliceSpec{
TimeSliceCondition: DatadogSLOTimeSliceCondition{
Threshold: resource.MustParse("5.0"),
},
Query: DatadogSLOTimeSliceQuery{
Formulas: []DatadogSLOFormula{
{Formula: "query1"},
},
Queries: []DatadogSLOQueryDefinition{
{
DataSource: "metrics",
Name: "query1",
Query: "avg:system.cpu.user{*}",
},
},
},
},
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("99.0"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSliceSpec.TimeSliceCondition.Comparator must be defined"),
},
{
name: "Missing formulas in time-slice query",
spec: &DatadogSLOSpec{
Name: "MyTimeSliceSLO",
TimeSliceSpec: &DatadogSLOTimeSliceSpec{
TimeSliceCondition: DatadogSLOTimeSliceCondition{
Comparator: ">",
Threshold: resource.MustParse("5.0"),
},
Query: DatadogSLOTimeSliceQuery{
Formulas: []DatadogSLOFormula{},
Queries: []DatadogSLOQueryDefinition{
{
DataSource: "metrics",
Name: "query1",
Query: "avg:system.cpu.user{*}",
},
},
},
},
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("99.0"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSliceSpec.Query.Formulas must contain at least one formula"),
},
{
name: "Empty formula string",
spec: &DatadogSLOSpec{
Name: "MyTimeSliceSLO",
TimeSliceSpec: &DatadogSLOTimeSliceSpec{
TimeSliceCondition: DatadogSLOTimeSliceCondition{
Comparator: ">",
Threshold: resource.MustParse("5.0"),
},
Query: DatadogSLOTimeSliceQuery{
Formulas: []DatadogSLOFormula{
{Formula: ""},
},
Queries: []DatadogSLOQueryDefinition{
{
DataSource: "metrics",
Name: "query1",
Query: "avg:system.cpu.user{*}",
},
},
},
},
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("99.0"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: errors.New("spec.TimeSliceSpec.Query.Formulas[0].Formula must not be empty"),
},
{
name: "Missing query fields",
spec: &DatadogSLOSpec{
Name: "MyTimeSliceSLO",
TimeSliceSpec: &DatadogSLOTimeSliceSpec{
TimeSliceCondition: DatadogSLOTimeSliceCondition{
Comparator: ">",
Threshold: resource.MustParse("5.0"),
},
Query: DatadogSLOTimeSliceQuery{
Formulas: []DatadogSLOFormula{
{Formula: "query1"},
},
Queries: []DatadogSLOQueryDefinition{
{
DataSource: "",
Name: "",
Query: "",
},
},
},
},
Type: DatadogSLOTypeTimeSlice,
TargetThreshold: resource.MustParse("99.0"),
Timeframe: DatadogSLOTimeFrame7d,
},
expected: utilserrors.NewAggregate(
[]error{
errors.New("spec.TimeSliceSpec.Query.Queries[0].DataSource must be defined"),
errors.New("spec.TimeSliceSpec.Query.Queries[0].Name must be defined"),
errors.New("spec.TimeSliceSpec.Query.Queries[0].Query must be defined"),
},
),
},
}

for _, tt := range tests {
Expand Down
75 changes: 72 additions & 3 deletions docs/datadog_slo.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ To deploy a `DatadogSLO` with the Datadog Operator, use the [`datadog-operator`
helm install my-datadog-operator datadog/datadog-operator -f values.yaml
```

2. Create a file with the spec of your `DatadogSLO` deployment configuration. An example configuration is:
2. Create a file with the spec of your `DatadogSLO` deployment configuration. The Datadog Operator supports three types of SLOs:


```
**Metric SLO Example:**
```yaml
apiVersion: datadoghq.com/v1alpha1
kind: DatadogSLO
metadata:
Expand All @@ -56,7 +56,55 @@ To deploy a `DatadogSLO` with the Datadog Operator, use the [`datadog-operator`
targetThreshold: "99.9"
timeframe: "7d"
type: "metric"
```

**Monitor SLO Example:**
```yaml
apiVersion: datadoghq.com/v1alpha1
kind: DatadogSLO
metadata:
name: example-monitor-slo
namespace: system
spec:
name: example-monitor-slo
description: "This is an example monitor SLO"
monitorIDs:
- 12345678
tags:
- "service:example"
- "env:prod"
targetThreshold: "99.5"
timeframe: "30d"
type: "monitor"
```

**Time-Slice SLO Example:**
```yaml
apiVersion: datadoghq.com/v1alpha1
kind: DatadogSLO
metadata:
name: example-time-slice-slo
namespace: system
spec:
name: example-time-slice-slo
description: "This is an example time-slice SLO"
type: "time_slice"
timeSliceSpec:
timeSliceCondition:
comparator: "<"
threshold: "5.0"
query:
formulas:
- formula: "query1"
queries:
- dataSource: "metrics"
name: "query1"
query: "avg:system.cpu.user{*}"
targetThreshold: "99.0"
timeframe: "7d"
tags:
- "team:infrastructure"
- "env:prod"
```

3. Deploy the `DatadogSLO` with the above configuration file:
Expand All @@ -69,6 +117,27 @@ To deploy a `DatadogSLO` with the Datadog Operator, use the [`datadog-operator`
Datadog Operator occasionally reconciles and keeps SLOs in line with the given configuration. There is also a force
sync every hour, so if a user deletes an SLO in the Datadog UI, Datadog Operator restores it in under an hour.

## DatadogSLO Spec

| Parameter | Description |
| --------- | ----------- |
| name | Name of the SLO |
| description | Description of the SLO |
| tags | Tags to associate with the SLO |
| type | Type of the SLO. Can be `metric`, `monitor`, or `time_slice` |
| query | Query for `metric` SLOs |
| monitorIDs | Monitor IDs for `monitor` SLOs |
| groups | Monitor groups for `monitor` SLOs (only valid when one monitor ID is provided) |
| timeSliceSpec | Time-slice specification for `time_slice` SLOs |
| timeSliceSpec.timeSliceCondition.comparator | Comparator for the time-slice condition (e.g., `<`, `>`, `<=`, `>=`) |
| timeSliceSpec.timeSliceCondition.threshold | Threshold value for the time-slice condition |
| timeSliceSpec.query.formulas | List of formulas for the time-slice query |
| timeSliceSpec.query.queries | List of query definitions with dataSource, name, and query |
| targetThreshold | Target threshold for the SLO |
| warningThreshold | Warning threshold for the SLO |
| timeframe | Timeframe for the SLO. Can be `7d`, `30d`, or `90d` |
| controllerOptions.disableRequiredTags | Disables the automatic addition of required tags to SLOs |

## Cleanup

The following commands delete the SLO from your Datadog account as well as all of the Kubernetes resources created by the previous instructions:
Expand Down
Loading
Loading