From fdb91824dfe2ebfb9248f4c373dbe736b81bac88 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:20:14 +0200 Subject: [PATCH 1/9] Add GetDetailedTestData for uptime test --- statuscake/uptime.go | 105 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/statuscake/uptime.go b/statuscake/uptime.go index 3da940c..11f32fe 100644 --- a/statuscake/uptime.go +++ b/statuscake/uptime.go @@ -14,14 +14,59 @@ import ( // UptimeCheckResult response from api type UptimeCheckResult struct { - TestID int `json:"TestID"` - Paused bool `json:"Paused"` - TestType string `json:"TestType"` - WebsiteName string `json:"WebsiteName"` - WebsiteURL string `json:"WebsiteURL"` - ContactID int `json:"ContactID"` - Status string `json:"Status"` - Uptime int `json:"Uptime"` + TestID int `json:"TestID"` + Paused bool `json:"Paused"` + TestType string `json:"TestType"` + WebsiteName string `json:"WebsiteName"` + WebsiteURL string `json:"WebsiteURL"` + ContactID int `json:"ContactID"` + Status string `json:"Status"` + Uptime float64 `json:"Uptime"` +} + +// UptimeCheckDetailed response from api +type UptimeCheckDetailed struct { + Method string `json:"Method"` + TestID int `json:"TestID"` + TestType string `json:"TestType"` + Paused bool `json:"Paused"` + WebsiteName string `json:"WebsiteName"` + URI string `json:"URI"` + ContactGroup string `json:"ContactGroup"` + ContactID int `json:"ContactID"` + ContactGroups []struct { + ID int `json:"ID"` + Name string `json:"Name"` + Email string `json:"Email"` + } `json:"ContactGroups"` + Status string `json:"Status"` + Tags []string `json:"Tags"` + Uptime int `json:"Uptime"` + CheckRate int `json:"CheckRate"` + Timeout int `json:"Timeout"` + LogoImage string `json:"LogoImage"` + Confirmation string `json:"Confirmation"` + FinalEndpoint string `json:"FinalEndpoint"` + WebsiteHost string `json:"WebsiteHost"` + NodeLocations []string `json:"NodeLocations"` + FindString string `json:"FindString"` + DoNotFind bool `json:"DoNotFind"` + LastTested string `json:"LastTested"` + NextLocation string `json:"NextLocation"` + Processing bool `json:"Processing"` + ProcessingState string `json:"ProcessingState"` + ProcessingOn string `json:"ProcessingOn"` + DownTimes string `json:"DownTimes"` + TriggerRate string `json:"TriggerRate"` + Sensitive bool `json:"Sensitive"` + EnableSSLWarning bool `json:"EnableSSLWarning"` + FollowRedirect bool `json:"FollowRedirect"` + Dnsip string `json:"DNSIP"` + DNSServer string `json:"DNSServer"` + CustomHeader string `json:"CustomHeader"` + PostRaw string `json:"PostRaw"` + UseJar int `json:"UseJar"` + StatusCodes []string `json:"StatusCodes"` } // ListUptime listing all uptime checks in the account @@ -142,3 +187,47 @@ func DeleteUptimeCheck(id int, api, user, key string) bool { } return true } + +// GetDetailedTestData gets detailed uptime data for test +func GetDetailedTestData(id int, api, user, key string) UptimeCheckDetailed { + client := &http.Client{} + request, _ := http.NewRequest("GET", api+"/API/Tests/Details/", nil) + // headers + request.Header.Add("Username", user) + request.Header.Add("API", key) + // params + q := request.URL.Query() + q.Add("TestID", strconv.Itoa(id)) + request.URL.RawQuery = q.Encode() + + resp, err := client.Do(request) + if err != nil { + log.Println(err) + return UptimeCheckDetailed{} + } + defer resp.Body.Close() + + // debug + // responseBody, _ := ioutil.ReadAll(resp.Body) + // log.Println(string(responseBody)) + + if resp.StatusCode != 200 { + message := helpers.ResolveStatusCode(resp.StatusCode) + log.Println(message) + return UptimeCheckDetailed{} + } + + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + } + + var uptimeCheckData UptimeCheckDetailed + e := json.Unmarshal(responseBody, &uptimeCheckData) + if e != nil { + log.Println(e) + log.Println("Error: Failed to parse response body") + } + + return uptimeCheckData +} From 696dbcd4be84ade59b88d62342af94ddc0c63275 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:20:33 +0200 Subject: [PATCH 2/9] Make lint happy --- statuscake/ssl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statuscake/ssl.go b/statuscake/ssl.go index b013409..e0a6593 100644 --- a/statuscake/ssl.go +++ b/statuscake/ssl.go @@ -91,7 +91,7 @@ func CreateSSLStatuscakeCheck(domain string, checkrate int, contacts string, api p := url.Values{} p.Add("domain", domain) - p.Add("checkrate", string(checkrate)) + p.Add("checkrate", fmt.Sprintf("%v", checkrate)) p.Add("contact_groups", contacts) p.Add("alert_at", "1,7,30") p.Add("alert_broken", "true") From b59be4264c45ebefd3c7a7a3cc8388ef066daa1d Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:21:00 +0200 Subject: [PATCH 3/9] Add ListPeriods for test --- statuscake/period.go | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 statuscake/period.go diff --git a/statuscake/period.go b/statuscake/period.go new file mode 100644 index 0000000..bf032d5 --- /dev/null +++ b/statuscake/period.go @@ -0,0 +1,78 @@ +package statuscake + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "statuscakectl/helpers" +) + +// Period object response from api +type Period struct { + Status string `json:"Status"` + StatusID string `json:"StatusID"` + Start string `json:"Start"` + End string `json:"End"` + StartUnix int `json:"Start_Unix"` + EndUnix int `json:"End_Unix"` + Additional string `json:"Additional"` + Period string `json:"Period"` +} + +type PeriodErrorMessage struct { + ErrNo int `json:"ErrNo"` + Access bool `json:"Access"` + Client string `json:"Client"` + TestID string `json:"TestID"` + Error string `json:"Error"` +} + +// ListPeriods listing all period objects for test +func ListPeriods(api, user, key string, testID int) []Period { + client := &http.Client{} + urlString := fmt.Sprintf("%s/API/Tests/Periods/?TestID=%v", api, testID) + request, _ := http.NewRequest("GET", urlString, nil) + request.Header.Add("Username", user) + request.Header.Add("API", key) + + resp, err := client.Do(request) + if err != nil { + log.Println(err) + } + defer resp.Body.Close() + + responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + } + var Periods []Period + + if resp.StatusCode != 200 { + message := helpers.ResolveStatusCode(resp.StatusCode) + log.Println(message) + return Periods + } + + e := json.Unmarshal(responseBody, &Periods) + if e != nil { + + // debug + // log.Println(string(responseBody)) + + var errorResponse PeriodErrorMessage + e = json.Unmarshal(responseBody, &errorResponse) + if e != nil { + log.Println("Failed to parse response body") + return nil + } + + log.Printf("Failed to get periods: %v", errorResponse.Error) + + return nil + + } + + return Periods +} From 4835471c1fa4711abaf2474e97d16365a22764f8 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:21:12 +0200 Subject: [PATCH 4/9] Add some helpers --- helpers/domain_validation.go | 9 +++++++++ helpers/helpers.go | 14 ++++++++++++++ helpers/helpers_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 helpers/helpers.go create mode 100644 helpers/helpers_test.go diff --git a/helpers/domain_validation.go b/helpers/domain_validation.go index b2ec459..b94c6e1 100644 --- a/helpers/domain_validation.go +++ b/helpers/domain_validation.go @@ -24,3 +24,12 @@ func DomainValidation(domain string) bool { return true } + +// GetHostnameFromUrl returns hostname from given url +func GetHostnameFromUrl(urlInput string) (string, error) { + u, err := url.Parse(urlInput) + if err != nil { + return "", err + } + return u.Hostname(), nil +} diff --git a/helpers/helpers.go b/helpers/helpers.go new file mode 100644 index 0000000..0bdc972 --- /dev/null +++ b/helpers/helpers.go @@ -0,0 +1,14 @@ +package helpers + +func Smallest(ff []float64) float64 { + if len(ff) < 1 { + return 0 + } + smallest := ff[0] + for _, f := range ff { + if f < smallest { + smallest = f + } + } + return smallest +} diff --git a/helpers/helpers_test.go b/helpers/helpers_test.go new file mode 100644 index 0000000..77fb051 --- /dev/null +++ b/helpers/helpers_test.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "testing" +) + +func Test_Smallest(t *testing.T) { + type args struct { + ff []float64 + } + tests := []struct { + name string + ff []float64 + want float64 + }{ + {name: "1", ff: []float64{0.1, -0.2, 4, 14, -2}, want: -2.0}, + {name: "2", ff: []float64{0.1, -0.2, 4, 14}, want: -0.2}, + {name: "3", ff: []float64{0.1, 0.2, 4, 14, 2}, want: 0.1}, + {name: "4", ff: []float64{2, 3, 4, 5, 1}, want: 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Smallest(tt.ff); got != tt.want { + t.Errorf("smallest() = %v, want %v", got, tt.want) + } + }) + } +} From a9486730d48c40cbd8309d3f94e12d951950d421 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:21:27 +0200 Subject: [PATCH 5/9] Add listperiods cmd --- cmd/list.go | 1 + cmd/list_periods.go | 70 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 cmd/list_periods.go diff --git a/cmd/list.go b/cmd/list.go index 419c5da..dd88bcf 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -16,4 +16,5 @@ var listCmd = &cobra.Command{ func init() { listCmd.AddCommand(listCmdSsl) listCmd.AddCommand(listCmdUptime) + listCmd.AddCommand(listCmdPeriods) } diff --git a/cmd/list_periods.go b/cmd/list_periods.go new file mode 100644 index 0000000..9d0afe7 --- /dev/null +++ b/cmd/list_periods.go @@ -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") +} From 38c91fcee21fed8ce369887faeef72201bcfbe20 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:21:42 +0200 Subject: [PATCH 6/9] Add calculate-sla command --- cmd/calculate_sla.go | 258 +++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + 2 files changed, 259 insertions(+) create mode 100644 cmd/calculate_sla.go diff --git a/cmd/calculate_sla.go b/cmd/calculate_sla.go new file mode 100644 index 0000000..f526d01 --- /dev/null +++ b/cmd/calculate_sla.go @@ -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, + } + +} diff --git a/cmd/root.go b/cmd/root.go index 96b2050..4fe0d04 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -32,6 +32,7 @@ func init() { rootCmd.AddCommand(listCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(deleteCmd) + rootCmd.AddCommand(calculateSlaCmd) // viper viper.AutomaticEnv() From 021768957a32f6227c146b19b95c7808287309a9 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:21:50 +0200 Subject: [PATCH 7/9] Update deps --- go.mod | 1 + go.sum | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 17b5807..a4015be 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module statuscakectl go 1.13 require ( + github.com/montanaflynn/stats v0.6.6 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.3.2 ) diff --git a/go.sum b/go.sum index 52c8213..fce56d7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -10,13 +11,15 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -43,6 +46,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From e09062f44c7c6cc8af7daac0677a925318e66703 Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Wed, 6 Apr 2022 10:25:47 +0200 Subject: [PATCH 8/9] Update readme --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 586754e..e5fc906 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. From b37be09344f2b5ec8d2366d1e0f5f1c46b49886b Mon Sep 17 00:00:00 2001 From: Pavel Voronov Date: Thu, 19 May 2022 13:06:12 +0200 Subject: [PATCH 9/9] Add option to name an uptime check --- cmd/create_uptime.go | 8 +++++++- statuscake/uptime.go | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cmd/create_uptime.go b/cmd/create_uptime.go index 3ff174b..157c8c9 100644 --- a/cmd/create_uptime.go +++ b/cmd/create_uptime.go @@ -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") @@ -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") @@ -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") diff --git a/statuscake/uptime.go b/statuscake/uptime.go index 11f32fe..71ef234 100644 --- a/statuscake/uptime.go +++ b/statuscake/uptime.go @@ -100,7 +100,7 @@ func ListUptime(api, user, key string) []UptimeCheckResult { } // CreateUptimeCheck create an uptime check -func CreateUptimeCheck(domain string, checkrate, timeout, confirmation, virus, donotfind, realbrowser, trigger, sslalert, follow int, contacts, testType, findstring, api, user, key string) bool { +func CreateUptimeCheck(websiteName, domain string, checkrate, timeout, confirmation, virus, donotfind, realbrowser, trigger, sslalert, follow int, contacts, testType, findstring, api, user, key string) bool { target, err := url.Parse(domain) if err != nil { fmt.Println("Please make sure to enter a valid domain (e.g https://www.domain.com)") @@ -112,7 +112,7 @@ func CreateUptimeCheck(domain string, checkrate, timeout, confirmation, virus, d } p := url.Values{} - p.Add("WebsiteName", domain) + p.Add("WebsiteName", websiteName) p.Add("WebsiteURL", domain) p.Add("CheckRate", strconv.Itoa(checkrate)) p.Add("Timeout", strconv.Itoa(timeout))