Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List test periods and calculate SLA #2

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ They offer a free plan and paid plans.

This is a small binary written in Golang which allows you to control statuscake via API.

Currently allows you to create/list/delete uptime tests and ssl monitoring.
Currently allows you to create/list/delete/calculate-sla uptime tests and ssl monitoring.

## Configuration

Expand All @@ -31,6 +31,8 @@ docker run -e STATUSCAKE_USER=your_statuscake_user -e STATUSCAKE_KEY=your_key om
# listing
statuscakectl list ssl
statuscakectl list uptime
statuscakectl list periods --domain www.domain.com
statuscakectl list periods --test-id 1111111

# create
statuscakectl create ssl -d domain.com
Expand All @@ -41,6 +43,12 @@ statuscakectl delete ssl -d domain.com
statuscakectl delete ssl --id 1111111
statuscakectl delete uptime -d https://www.domain.com
statuscakectl delete uptime --id 1111111

# calculate-sla
statuscakectl calculate-sla --domains testdomain.com
statuscakectl calculate-sla --domains foo.com,bar.org --from 2021-12-01 -to 2022-01-01
statuscakectl calculate-sla --domains foo.com,bar.org --maintenance-start-hour 0 --maintenance-finish-hour 2

```

If you'd like to test them out I would appriciate it if you do it via this affiliation [link](https://www.statuscake.com/statuscake-long-page/?a_aid=5d6fc4349afd6&a_bid=af013c39) to help support my time working on this cool tool.
Expand Down
258 changes: 258 additions & 0 deletions cmd/calculate_sla.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package cmd

import (
"fmt"
"log"
"sort"
"statuscakectl/helpers"
"statuscakectl/statuscake"
"time"

"github.com/montanaflynn/stats"
"github.com/spf13/cobra"
)

var debug bool

var calculateSlaCmd = &cobra.Command{
Use: "calculate-sla",
Short: "calculates sla for domain from to date",
Example: `statuscakectl calculate-sla --domains testdomain.com
statuscakectl calculate-sla --domains foo.com,bar.org --from 2021-12-01 -to 2022-01-01
statuscakectl calculate-sla --domains foo.com,bar.org --maintenance-start-hour 0 --maintenance-finish-hour 2`,
Run: func(cmd *cobra.Command, args []string) {
api, _ := cmd.Flags().GetString("api")
user, _ := cmd.Flags().GetString("user")
key, _ := cmd.Flags().GetString("key")
domains, _ := cmd.Flags().GetStringSlice("domains")
fromString, _ := cmd.Flags().GetString("from")
toString, _ := cmd.Flags().GetString("to")

mStartHour, _ := cmd.Flags().GetInt("maintenance-start-hour")
mEndHour, _ := cmd.Flags().GetInt("maintenance-finish-hour")

from := parseDateToUnix(fromString)
to := parseDateToUnix(toString)
testID := 0
var slas []float64

if from > to && to != 0 {
fmt.Println("to date should be after from")
return
}

if len(domains) < 1 {
fmt.Println("Please make sure to provide a valid domains flag")
return
}

var allPeriods []statuscake.Period

uptimeTests := statuscake.ListUptime(api, user, key)

for _, domain := range domains {
if domain != "" {
for _, t := range uptimeTests {
hostname, err := helpers.GetHostnameFromUrl(t.WebsiteURL)
if err != nil {
log.Printf("Can't get hostname from this URL:%v\n", t.WebsiteURL)
}
if hostname == domain {
testID = t.TestID
break
}
}
if testID < 1 {
fmt.Printf("Cannot find test for domain: %v\n", domain)
return
}
}
periods := statuscake.ListPeriods(api, user, key, testID)
allPeriods = append(allPeriods, periods...)

from, to, sla := calculateSLA(mStartHour, mEndHour, int(from), int(to), periods)
slas = append(slas, sla)
fmt.Printf("SLA for %v from:%v to:%v is %0.2f\n", domain, unixDateToString(from), unixDateToString(to), sla)
}

fmt.Printf("Worst SLA: %0.2f\n", helpers.Smallest(slas))
meanSla, err := stats.Mean(slas)
if err != nil {
fmt.Printf("Can't calculate mean sla due to:%v\n", meanSla)
} else {
fmt.Printf("Mean SLA: %0.2f\n", meanSla)
}

medianSla, err := stats.Median(slas)
if err != nil {
fmt.Printf("Can't calculate median sla due to:%v\n", medianSla)
} else {
fmt.Printf("Median SLA: %0.2f\n", medianSla)
}

_, _, sla := calculateSLA(mStartHour, mEndHour, int(from), int(to), allPeriods)
fmt.Printf("Combined down SLA: %0.2f\n", sla)

},
}

func init() {
// flags
calculateSlaCmd.Flags().StringSliceP("domains", "d", []string{}, "Domain names to find periods for (eg. foo.com or foo.com,bar.com")
calculateSlaCmd.Flags().String("from", "", "Date from which calculate SLA (Format: \"2006-01-02\"")
calculateSlaCmd.Flags().String("to", "", "Date to which calculate SLA (Format: \"2006-01-02\"")
calculateSlaCmd.Flags().Int("maintenance-start-hour", 0, "Periodic maintenance start hour in UTC (experimental)")
calculateSlaCmd.Flags().Int("maintenance-finish-hour", 0, "Periodic maintenance end hour in UTC (experimental)")
}

func unixDateToString(u int64) string {
return time.Unix(u, 0).Format("2006-01-02")
}

func parseDateToUnix(s string) int64 {
if s == "" {
return 0
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
log.Fatalln("Cannot parse given date:", s)
}
return t.Unix()
}

// calculateSLA takes from and to dates in unixseconds format and returns real from, real to and sla
func calculateSLA(mStart, mEnd int, from, to int, periods []statuscake.Period) (int64, int64, float64) {
sort.Slice(periods, func(i, j int) bool {
return periods[i].StartUnix < periods[j].StartUnix
})

first := getFirstTime(periods)
if first > from {
from = first
}

if to > int(time.Now().Unix()) || to < 1 {
to = int(time.Now().Unix())
}

totalSeconds := to - from
var downSeconds int
for _, p := range periods {

if p.EndUnix < from {
continue
}

if p.StartUnix > to {
continue
}

if p.Status == "Down" {
if p.StartUnix < from {
downSeconds = downSeconds + p.EndUnix - from
} else if p.EndUnix > to {
downSeconds = downSeconds + to - p.StartUnix
} else {
downSeconds = downSeconds + p.EndUnix - p.StartUnix
}

maintenanceWindowSeconds := secondsCoveredByMaintenanceWindow(p, mStart, mEnd)
downSeconds = downSeconds - maintenanceWindowSeconds

}

}
sla := 100 - ((100 * float64(downSeconds)) / float64(totalSeconds))
return int64(from), int64(to), sla
}

// finds first time check
func getFirstTime(periods []statuscake.Period) int {
if len(periods) < 1 {
return 0
}
first := periods[0].StartUnix
for _, p := range periods {
if p.StartUnix < first {
first = p.StartUnix
}
}
return first
}

func secondsCoveredByMaintenanceWindow(p statuscake.Period, startHour, endHour int) int {
startTime := time.Unix(int64(p.StartUnix), 0)
endTime := time.Unix(int64(p.EndUnix), 0)

var mww []maintenanceWindow

for i := 0.0; i < endTime.Sub(startTime).Hours(); i = i + 24 {
date := startTime.Add(time.Duration(i) * time.Hour)
mww = append(mww, maintenanceWindowForDate(date, startHour, endHour))
}

var secondsCoveredByMaintenanceWindow int

for _, mw := range mww {
// if period is fully within maintenance window
if startTime.After(mw.start) && endTime.Before(mw.end) {
secondsCoveredByMaintenanceWindow = secondsCoveredByMaintenanceWindow + p.EndUnix - p.StartUnix
continue
}

// if period starts within maintenance window
if startTime.After(mw.start) && startTime.Before(mw.end) {
secondsCoveredByMaintenanceWindow = secondsCoveredByMaintenanceWindow + int(mw.end.Unix()) - int(startTime.Unix())
continue
}

// if period ends within maintenance window
if endTime.After(mw.start) && endTime.Before(mw.end) {
secondsCoveredByMaintenanceWindow = secondsCoveredByMaintenanceWindow + int(endTime.Unix()) - int(mw.start.Unix())
continue
}

// if maintenance windows is within period
if startTime.Before(mw.start) && endTime.After(mw.end) {
secondsCoveredByMaintenanceWindow = secondsCoveredByMaintenanceWindow + int(mw.start.Unix()) - int(mw.end.Unix())
continue
}
}

return secondsCoveredByMaintenanceWindow
}

type maintenanceWindow struct {
start time.Time
end time.Time
}

func maintenanceWindowForDate(date time.Time, startHour, endHour int) maintenanceWindow {
start := time.Date(
date.Year(),
date.Month(),
date.Day(),
startHour,
0,
0,
0,
time.UTC,
)

end := time.Date(
date.Year(),
date.Month(),
date.Day(),
endHour,
0,
0,
0,
time.UTC,
)

return maintenanceWindow{
start: start,
end: end,
}

}
8 changes: 7 additions & 1 deletion cmd/create_uptime.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ var createCmdUptime = &cobra.Command{
cmd.Usage()
return
}
name, _ := cmd.Flags().GetString("name")
if name == "" {
name = domain
}

checkrate, _ := cmd.Flags().GetInt("checkrate")
timeout, _ := cmd.Flags().GetInt("timeout")
confirmation, _ := cmd.Flags().GetInt("confirmation")
Expand All @@ -45,7 +50,7 @@ var createCmdUptime = &cobra.Command{
return
}

createCheck := statuscake.CreateUptimeCheck(domain, checkrate, timeout, confirmation, virus,
createCheck := statuscake.CreateUptimeCheck(name, domain, checkrate, timeout, confirmation, virus,
donotfind, realbrowser, trigger, sslalert, follow, contacts, testType, findstring, api, user, key)
if !createCheck {
fmt.Println("Failed to create uptime check")
Expand All @@ -64,6 +69,7 @@ func init() {
createCmdUptime.Flags().Int("confirmation", 1, "Confimation servers before alert (default 1)")
createCmdUptime.Flags().Int("virus", 1, "Enable virus checking or not. default 1 = enable")
createCmdUptime.Flags().String("findstring", "", "A string that should either be found or not found")
createCmdUptime.Flags().String("name", "", "A name of the test")
createCmdUptime.Flags().Int("donotfind", 0, "If the above string should be found to trigger a alert. 1 = will trigger if FindString found")
createCmdUptime.Flags().StringP("type", "t", "HTTP", "Type of test type to use: HTTP,TCP,PING (default HTTP)")
createCmdUptime.MarkFlagRequired("type")
Expand Down
1 change: 1 addition & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ var listCmd = &cobra.Command{
func init() {
listCmd.AddCommand(listCmdSsl)
listCmd.AddCommand(listCmdUptime)
listCmd.AddCommand(listCmdPeriods)
}
70 changes: 70 additions & 0 deletions cmd/list_periods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmd

import (
"fmt"
"log"
"sort"
"statuscakectl/helpers"
"statuscakectl/statuscake"

"github.com/spf13/cobra"
)

var listCmdPeriods = &cobra.Command{
Use: "periods",
Short: "list periods for test-id or domain (if both provided, looks for id of the domain test)",
Example: "statuscakectl list periods --test-id 1111\nstatuscakectl list periods --domain testdomain.com",
Run: func(cmd *cobra.Command, args []string) {
api, _ := cmd.Flags().GetString("api")
user, _ := cmd.Flags().GetString("user")
key, _ := cmd.Flags().GetString("key")
testID, _ := cmd.Flags().GetInt("test-id")
domain, _ := cmd.Flags().GetString("domain")

if testID < 1 && domain == "" {
fmt.Println("Please make sure to provide a valid test-id or domain flags")
return
}

if domain != "" {
uptimeTests := statuscake.ListUptime(api, user, key)
for _, t := range uptimeTests {
hostname, err := helpers.GetHostnameFromUrl(t.WebsiteURL)
if err != nil {
log.Printf("Can't get hostname from this URL:%v\n", t.WebsiteURL)
}
if hostname == domain {
testID = t.TestID
break
}
}
if testID < 1 {
fmt.Printf("Cannot find test for domain: %v\n", domain)
return
}
}

detailedData := statuscake.GetDetailedTestData(testID, api, user, key)
domain, err := helpers.GetHostnameFromUrl(detailedData.URI)
if err != nil {
log.Fatalf("Cant get hostname from URI:%v", detailedData.URI)
}

periods := statuscake.ListPeriods(api, user, key, testID)

sort.Slice(periods, func(i, j int) bool {
return periods[i].StartUnix < periods[j].StartUnix
})

fmt.Printf("Total %v checks in statuscake account:\n", len(periods))
for _, r := range periods {
fmt.Printf("%v was %v for %v from %v to %v\n", domain, r.Status, r.Period, r.Start, r.End)
}
},
}

func init() {
// flags
listCmdPeriods.Flags().Int("test-id", 0, "TestID of the test you want to get periods for")
listCmdPeriods.Flags().StringP("domain", "d", "", "Domain name to find periods for")
}
Loading