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

SLO: CRUD API Functionality #145

Merged
merged 15 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
155 changes: 155 additions & 0 deletions slo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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"`
}

type Slo struct {
elainevuong marked this conversation as resolved.
Show resolved Hide resolved
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
Service string `json:"service,omitempty"`
Query Query `json:"query"`
Alerting *Alerting `json:"alerting,omitempty"`
Labels *[]Label `json:"labels,omitempty"`
Objectives []Objective `json:"objectives"`
DrilldownDashboardRef *DashboardRef `json:"drillDownDashboardRef,omitempty"`
}

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

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

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

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

type DashboardRef struct {
UID string `json:"uid,omitempty"`
}

type FreeformQuery struct {
Query string `json:"freeformQuery,omitempty"`
}

type ThresholdQuery struct {
ThresholdMetric *MetricDef `json:"thresholdMetric,omitempty"`
}

type RatioQuery struct {
SuccessMetric *MetricDef `json:"successMetric,omitempty"`
TotalMetric *MetricDef `json:"totalMetric,omitempty"`
}

type PercentileQuery struct {
HistogramMetric *MetricDef `json:"histogramMetric,omitempty"`
Percentile float64 `json:"percentile,omitempty"`
}

type Threshold struct {
Value float64 `json:"value,omitempty"`
Operator string `json:"operator,omitempty"`
}

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

type Query struct {
ThresholdQuery
RatioQuery
PercentileQuery
FreeformQuery
Threshold *Threshold `json:"threshold,omitempty"`
GroupByLabels []string `json:"groupBy,omitempty"`
}

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
}
170 changes: 170 additions & 0 deletions slo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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": {
"freeformQuery": "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"
},
"objectives": [
{
"value": 0.995,
"window": "28d"
}
],
"drillDownDashboardRef": {
"uid": "5IkqX6P4k"
}
}
]
}`

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

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

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

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

return slo
}