Skip to content
This repository has been archived by the owner on Jan 15, 2024. It is now read-only.

Commit

Permalink
SLO: CRUD API Functionality (#145)
Browse files Browse the repository at this point in the history
* ListSLOs functionality retrives all SLOs

* CreateSLO and GetSLO Functionality implemented

* DeleteSLO Functionality implemented

* UpdateSLO Functionality implemented

* UpdateSLO Functionality corrected

* Update Linting Errors

* Removes Unnecessary Comments

* Update Variable Naming

* Lint Checker

* Update Tests for SLOs

* Updating Slo Types with a comment to original source files

* Updated SLO Types and Tests
  • Loading branch information
elainevuong authored May 4, 2023
1 parent 44fa23d commit 3f84a92
Show file tree
Hide file tree
Showing 2 changed files with 357 additions and 0 deletions.
177 changes: 177 additions & 0 deletions slo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Slo types lifted from github.com/grafana/slo/pkg/generated/models/slo/slo_type_gen.go
package gapi

import (
"bytes"
"encoding/json"
"fmt"
)

var sloPath string = "/api/plugins/grafana-slo-app/resources/v1/slo"

type Slos struct {
Slos []Slo `json:"slos"`
}

const (
QueryTypeFreeform QueryType = "freeform"
QueryTypeHistogram QueryType = "histogram"
QueryTypeRatio QueryType = "ratio"
QueryTypeThreshold QueryType = "threshold"
)

const (
ThresholdOperatorEmpty ThresholdOperator = "<"
ThresholdOperatorEqualEqual ThresholdOperator = "=="
ThresholdOperatorN1 ThresholdOperator = "<="
ThresholdOperatorN2 ThresholdOperator = ">="
ThresholdOperatorN3 ThresholdOperator = ">"
)

type Alerting struct {
Annotations []Label `json:"annotations,omitempty"`
FastBurn *AlertingMetadata `json:"fastBurn,omitempty"`
Labels []Label `json:"labels,omitempty"`
SlowBurn *AlertingMetadata `json:"slowBurn,omitempty"`
}

type AlertingMetadata struct {
Annotations []Label `json:"annotations,omitempty"`
Labels []Label `json:"labels,omitempty"`
}

type DashboardRef struct {
UID string `json:"UID"`
}

type FreeformQuery struct {
Query string `json:"query"`
}

type HistogramQuery struct {
GroupByLabels []string `json:"groupByLabels,omitempty"`
Metric MetricDef `json:"metric"`
Percentile float64 `json:"percentile"`
Threshold Threshold `json:"threshold"`
}

type Label struct {
Key string `json:"key"`
Value string `json:"value"`
}

type MetricDef struct {
PrometheusMetric string `json:"prometheusMetric"`
Type *string `json:"type,omitempty"`
}

type Objective struct {
Value float64 `json:"value"`
Window string `json:"window"`
}

type Query struct {
Freeform *FreeformQuery `json:"freeform,omitempty"`
Histogram *HistogramQuery `json:"histogram,omitempty"`
Ratio *RatioQuery `json:"ratio,omitempty"`
Threshold *ThresholdQuery `json:"threshold,omitempty"`
Type QueryType `json:"type"`
}

type QueryType string

type RatioQuery struct {
GroupByLabels []string `json:"groupByLabels,omitempty"`
SuccessMetric MetricDef `json:"successMetric"`
TotalMetric MetricDef `json:"totalMetric"`
}

type Slo struct {
Alerting *Alerting `json:"alerting,omitempty"`
Description string `json:"description"`
DrillDownDashboardRef *DashboardRef `json:"drillDownDashboardRef,omitempty"`
Labels []Label `json:"labels,omitempty"`
Name string `json:"name"`
Objectives []Objective `json:"objectives"`
Query Query `json:"query"`
UUID string `json:"uuid"`
}

type Threshold struct {
Operator ThresholdOperator `json:"operator"`
Value float64 `json:"value"`
}

type ThresholdOperator string

type ThresholdQuery struct {
GroupByLabels []string `json:"groupByLabels,omitempty"`
Metric MetricDef `json:"metric"`
Threshold Threshold `json:"threshold"`
}

type CreateSLOResponse struct {
Message string `json:"message,omitempty"`
UUID string `json:"uuid,omitempty"`
}

// ListSlos retrieves a list of all Slos
func (c *Client) ListSlos() (Slos, error) {
var slos Slos

if err := c.request("GET", sloPath, nil, nil, &slos); err != nil {
return Slos{}, err
}

return slos, nil
}

// GetSLO returns a single Slo based on its uuid
func (c *Client) GetSlo(uuid string) (Slo, error) {
var slo Slo
path := fmt.Sprintf("%s/%s", sloPath, uuid)

if err := c.request("GET", path, nil, nil, &slo); err != nil {
return Slo{}, err
}

return slo, nil
}

// CreateSLO creates a single Slo
func (c *Client) CreateSlo(slo Slo) (CreateSLOResponse, error) {
response := CreateSLOResponse{}

data, err := json.Marshal(slo)
if err != nil {
return response, err
}

if err := c.request("POST", sloPath, nil, bytes.NewBuffer(data), &response); err != nil {
return CreateSLOResponse{}, err
}

return response, err
}

// DeleteSLO deletes the Slo with the passed in UUID
func (c *Client) DeleteSlo(uuid string) error {
path := fmt.Sprintf("%s/%s", sloPath, uuid)
return c.request("DELETE", path, nil, nil, nil)
}

// UpdateSLO updates the Slo with the passed in UUID and Slo
func (c *Client) UpdateSlo(uuid string, slo Slo) error {
path := fmt.Sprintf("%s/%s", sloPath, uuid)

data, err := json.Marshal(slo)
if err != nil {
return err
}

if err := c.request("PUT", path, nil, bytes.NewBuffer(data), nil); err != nil {
return err
}

return nil
}
180 changes: 180 additions & 0 deletions slo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package gapi

import (
"testing"

"github.com/gobs/pretty"
)

func TestSLOs(t *testing.T) {
t.Run("list all SLOs succeeds", func(t *testing.T) {
client := gapiTestTools(t, 200, getSlosJSON)

resp, err := client.ListSlos()

slos := resp.Slos

if err != nil {
t.Error(err)
}
t.Log(pretty.PrettyFormat(slos))
if len(slos) != 1 {
t.Errorf("wrong number of contact points returned, got %d", len(slos))
}
if slos[0].Name != "list-slos" {
t.Errorf("incorrect name - expected Name-Test, got %s", slos[0].Name)
}
})

t.Run("get individual SLO succeeds", func(t *testing.T) {
client := gapiTestTools(t, 200, getSloJSON)

slo, err := client.GetSlo("qkkrknp12w6tmsdcrfkdf")

t.Log(pretty.PrettyFormat(slo))
if err != nil {
t.Error(err)
}
if slo.UUID != "qkkrknp12w6tmsdcrfkdf" {
t.Errorf("incorrect UID - expected qkkrknp12w6tmsdcrfkdf, got %s", slo.UUID)
}
})

t.Run("get non-existent SLOs fails", func(t *testing.T) {
client := gapiTestTools(t, 404, getSlosJSON)

slo, err := client.GetSlo("qkkrknp12w6tmsdcrfkdf")

if err == nil {
t.Log(pretty.PrettyFormat(slo))
t.Error("expected error but got nil")
}
})

t.Run("create SLO succeeds", func(t *testing.T) {
client := gapiTestTools(t, 201, createSloJSON)
slo := generateSlo()

resp, err := client.CreateSlo(slo)

if err != nil {
t.Error(err)
}
if resp.UUID != "sjnp8wobcbs3eit28n8yb" {
t.Errorf("unexpected UID returned, got %s", resp.UUID)
}
})

t.Run("update SLO succeeds", func(t *testing.T) {
client := gapiTestTools(t, 200, createSloJSON)
slo := generateSlo()
slo.Description = "Updated Description"

err := client.UpdateSlo(slo.UUID, slo)

if err != nil {
t.Error(err)
}
})

t.Run("delete SLO succeeds", func(t *testing.T) {
client := gapiTestTools(t, 204, "")

err := client.DeleteSlo("qkkrknp12w6tmsdcrfkdf")

if err != nil {
t.Log(err)
t.Error(err)
}
})
}

const getSlosJSON = `
{
"slos": [
{
"uuid": "qkkrknp12w6tmsdcrfkdf",
"name": "list-slos",
"description": "list-slos-description",
"query": {
"freeform": {
"query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"
},
"type": "freeform"
},
"objectives": [
{
"value": 0.995,
"window": "28d"
}
],
"drillDownDashboardRef": {
"uid": "5IkqX6P4k"
}
}
]
}`

const getSloJSON = `
{
"uuid": "qkkrknp12w6tmsdcrfkdf",
"name": "Name-Test",
"description": "Description-Test",
"query": {
"freeform": {
"query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"
},
"type": "freeform"
},
"objectives": [
{
"value": 0.995,
"window": "28d"
}
],
"drillDownDashboardRef": {
"uid": "5IkqX6P4k"
}
}`

const createSloJSON = `
{
"uuid": "sjnp8wobcbs3eit28n8yb",
"name": "test-name",
"description": "test-description",
"query": {
"freeform": {
"query": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"
},
"type": "freeform"
},
"objectives": [
{
"value": 0.995,
"window": "30d"
}
],
"drillDownDashboardRef": {
"uid": "zz5giRyVk"
}
}
`

func generateSlo() Slo {
objective := []Objective{{Value: 0.995, Window: "30d"}}
query := Query{
Freeform: &FreeformQuery{
Query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))",
},
Type: QueryTypeFreeform,
}

slo := Slo{
Name: "test-name",
Description: "test-description",
Objectives: objective,
Query: query,
}

return slo
}

0 comments on commit 3f84a92

Please sign in to comment.