From 7f05fbcda582a9da9885437dde816c2304933300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Frange=C5=BE?= Date: Sat, 3 Oct 2020 23:39:23 +0200 Subject: [PATCH 1/5] Implemented UptimeRobot widget This is the first working version of the UptimeRobot module, as discussed in #979 --- app/widget_maker.go | 4 + modules/uptimerobot/keyboard.go | 6 ++ modules/uptimerobot/settings.go | 35 +++++++ modules/uptimerobot/widget.go | 157 ++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 modules/uptimerobot/keyboard.go create mode 100644 modules/uptimerobot/settings.go create mode 100644 modules/uptimerobot/widget.go diff --git a/app/widget_maker.go b/app/widget_maker.go index 6f7275a8..e787e2a2 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -66,6 +66,7 @@ import ( "github.com/wtfutil/wtf/modules/twitter" "github.com/wtfutil/wtf/modules/twitterstats" "github.com/wtfutil/wtf/modules/unknown" + "github.com/wtfutil/wtf/modules/uptimerobot" "github.com/wtfutil/wtf/modules/victorops" "github.com/wtfutil/wtf/modules/weatherservices/arpansagovau" "github.com/wtfutil/wtf/modules/weatherservices/prettyweather" @@ -292,6 +293,9 @@ func MakeWidget( case "twitterstats": settings := twitterstats.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = twitterstats.NewWidget(app, pages, settings) + case "uptimerobot": + settings := uptimerobot.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = uptimerobot.NewWidget(app, pages, settings) case "victorops": settings := victorops.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = victorops.NewWidget(app, settings) diff --git a/modules/uptimerobot/keyboard.go b/modules/uptimerobot/keyboard.go new file mode 100644 index 00000000..483a4ef2 --- /dev/null +++ b/modules/uptimerobot/keyboard.go @@ -0,0 +1,6 @@ +package uptimerobot + +func (widget *Widget) initializeKeyboardControls() { + widget.SetKeyboardChar("/", widget.ShowHelp, "Show/hide this help widget") + widget.SetKeyboardChar("r", widget.Refresh, "Refresh widget") +} diff --git a/modules/uptimerobot/settings.go b/modules/uptimerobot/settings.go new file mode 100644 index 00000000..fe638ee1 --- /dev/null +++ b/modules/uptimerobot/settings.go @@ -0,0 +1,35 @@ +package uptimerobot + +import ( + "os" + + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = true + defaultTitle = "Uptime Robot" +) + +type Settings struct { + common *cfg.Common + + apiKey string `help:"An UptimeRobot API key."` + uptimePeriods string `help:"The periods over which to display uptime (in days, dash-separated)." optional:"true"` +} + +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + apiKey: ymlConfig.UString("apiKey", os.Getenv("WTF_UPTIMEROBOT_APIKEY")), + uptimePeriods: ymlConfig.UString("uptimePeriods", "30"), + } + + cfg.ModuleSecret(name, globalConfig, &settings.apiKey). + Service("https://api.uptimerobot.com").Load() + + return &settings +} diff --git a/modules/uptimerobot/widget.go b/modules/uptimerobot/widget.go new file mode 100644 index 00000000..f9917b5e --- /dev/null +++ b/modules/uptimerobot/widget.go @@ -0,0 +1,157 @@ +package uptimerobot + +import ( + "fmt" + "errors" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +type Widget struct { + view.KeyboardWidget + view.ScrollableWidget + + monitors []Monitor + settings *Settings + err error +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := &Widget{ + KeyboardWidget: view.NewKeyboardWidget(app, pages, settings.common), + ScrollableWidget: view.NewScrollableWidget(app, settings.common), + + settings: settings, + } + + widget.SetRenderFunction(widget.Render) + widget.initializeKeyboardControls() + widget.View.SetInputCapture(widget.InputCapture) + + widget.KeyboardWidget.SetView(widget.View) + + return widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + monitors, err := widget.getMonitors() + widget.monitors = monitors + widget.err = err + widget.SetItemCount(len(monitors)) + + widget.Render() +} + +// Render sets up the widget data for redrawing to the screen +func (widget *Widget) Render() { + widget.Redraw(widget.content) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) content() (string, string, bool) { + numUp := 0 + for _, monitor := range widget.monitors { + if monitor.State == 2 { + numUp++ + } + } + + title := fmt.Sprintf("UptimeRobot (%d/%d)", numUp, len(widget.monitors)) + + if widget.err != nil { + return title, widget.err.Error(), true + } + + if widget.monitors == nil { + return title, "No monitors to display", false + } + + str := widget.contentFrom(widget.monitors) + + return title, str, false +} + +func (widget *Widget) contentFrom(monitors []Monitor) string { + var str string + + for _, monitor := range monitors { + prefix := "" + + switch monitor.State { + case 2: + prefix += "[green] + " + break + case 8: + case 9: + prefix += "[red] - " + break + default: + prefix += "[yellow] ~ " + } + + str += fmt.Sprintf(`%s%s [gray](%s)[white] +`, + prefix, + monitor.Name, + monitor.Uptime, + ) + } + + return str +} + +type Monitor struct { + Name string `json:"friendly_name"` + // Monitor state, see: https://uptimerobot.com/api/#parameters + State int8 `json:"status"` + // Uptime ratio, preformatted, e.g.: 100.000-97.233-96.975 + Uptime string `json:"custom_uptime_ratio"` +} + +func (widget *Widget) getMonitors() ([]Monitor, error) { + // See: https://uptimerobot.com/api/#getMonitorsWrap + resp, err_h := http.PostForm("https://api.uptimerobot.com/v2/getMonitors", + url.Values{ + "api_key": {widget.settings.apiKey}, + "format": {"json"}, + "custom_uptime_ratios": {widget.settings.uptimePeriods}, + }, + ) + + if err_h != nil { + return nil, err_h + } + + body, _ := ioutil.ReadAll(resp.Body) + + // First pass to read the status + c := make(map[string]json.RawMessage) + json.Unmarshal([]byte(body), &c) + + stat := string(c["stat"]) + if stat != `"ok"` { + return nil, errors.New(string(body)) + } + + // Second pass to get the actual info + var monitors []Monitor + err_j := json.Unmarshal(c["monitors"], &monitors) + + if err_j != nil { + return nil, err_j + } + + return monitors, nil +} From 6908e744e14f469514e91949d896691748362623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Frange=C5=BE?= Date: Sun, 4 Oct 2020 10:51:39 +0200 Subject: [PATCH 2/5] Make CI happy --- modules/uptimerobot/widget.go | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/modules/uptimerobot/widget.go b/modules/uptimerobot/widget.go index f9917b5e..ec8cc96d 100644 --- a/modules/uptimerobot/widget.go +++ b/modules/uptimerobot/widget.go @@ -1,9 +1,9 @@ package uptimerobot import ( - "fmt" - "errors" "encoding/json" + "errors" + "fmt" "io/ioutil" "net/http" "net/url" @@ -92,11 +92,9 @@ func (widget *Widget) contentFrom(monitors []Monitor) string { switch monitor.State { case 2: prefix += "[green] + " - break case 8: case 9: prefix += "[red] - " - break default: prefix += "[yellow] ~ " } @@ -113,9 +111,9 @@ func (widget *Widget) contentFrom(monitors []Monitor) string { } type Monitor struct { - Name string `json:"friendly_name"` + Name string `json:"friendly_name"` // Monitor state, see: https://uptimerobot.com/api/#parameters - State int8 `json:"status"` + State int8 `json:"status"` // Uptime ratio, preformatted, e.g.: 100.000-97.233-96.975 Uptime string `json:"custom_uptime_ratio"` } @@ -124,8 +122,8 @@ func (widget *Widget) getMonitors() ([]Monitor, error) { // See: https://uptimerobot.com/api/#getMonitorsWrap resp, err_h := http.PostForm("https://api.uptimerobot.com/v2/getMonitors", url.Values{ - "api_key": {widget.settings.apiKey}, - "format": {"json"}, + "api_key": {widget.settings.apiKey}, + "format": {"json"}, "custom_uptime_ratios": {widget.settings.uptimePeriods}, }, ) @@ -138,19 +136,22 @@ func (widget *Widget) getMonitors() ([]Monitor, error) { // First pass to read the status c := make(map[string]json.RawMessage) - json.Unmarshal([]byte(body), &c) + err_j1 := json.Unmarshal([]byte(body), &c) - stat := string(c["stat"]) - if stat != `"ok"` { + if err_j1 != nil { + return nil, err_j1 + } + + if string(c["stat"]) != `"ok"` { return nil, errors.New(string(body)) } // Second pass to get the actual info var monitors []Monitor - err_j := json.Unmarshal(c["monitors"], &monitors) + err_j2 := json.Unmarshal(c["monitors"], &monitors) - if err_j != nil { - return nil, err_j + if err_j2 != nil { + return nil, err_j2 } return monitors, nil From dd4899a07ce239d9c4110f7227f8c90ef785cbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Frange=C5=BE?= Date: Mon, 5 Oct 2020 11:08:30 +0200 Subject: [PATCH 3/5] UptimeRobot: improve uptime formatting --- modules/uptimerobot/widget.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/modules/uptimerobot/widget.go b/modules/uptimerobot/widget.go index ec8cc96d..1827a1e3 100644 --- a/modules/uptimerobot/widget.go +++ b/modules/uptimerobot/widget.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/url" + "strings" "github.com/rivo/tview" "github.com/wtfutil/wtf/view" @@ -103,13 +104,28 @@ func (widget *Widget) contentFrom(monitors []Monitor) string { `, prefix, monitor.Name, - monitor.Uptime, + formatUptimes(monitor.Uptime), ) } return str } +func formatUptimes(str string) string { + splits := strings.Split(str, "-") + str = "" + for i, s := range splits { + if i != 0 { + str += "|" + } + s = s[:5] + s = strings.TrimRight(s, "0") + s = strings.TrimRight(s, ".") + "%" + str += s + } + return str +} + type Monitor struct { Name string `json:"friendly_name"` // Monitor state, see: https://uptimerobot.com/api/#parameters From b8c9f96db2460bd1bab6bb7fb4958525be029f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Frange=C5=BE?= Date: Mon, 5 Oct 2020 11:45:49 +0200 Subject: [PATCH 4/5] UptimeRobot: option to show offline monitors first --- modules/uptimerobot/settings.go | 2 ++ modules/uptimerobot/widget.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/modules/uptimerobot/settings.go b/modules/uptimerobot/settings.go index fe638ee1..878e4fa5 100644 --- a/modules/uptimerobot/settings.go +++ b/modules/uptimerobot/settings.go @@ -17,6 +17,7 @@ type Settings struct { apiKey string `help:"An UptimeRobot API key."` uptimePeriods string `help:"The periods over which to display uptime (in days, dash-separated)." optional:"true"` + offlineFirst bool `help:"Display offline monitors at the top." optional:"true"` } func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { @@ -26,6 +27,7 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co apiKey: ymlConfig.UString("apiKey", os.Getenv("WTF_UPTIMEROBOT_APIKEY")), uptimePeriods: ymlConfig.UString("uptimePeriods", "30"), + offlineFirst: ymlConfig.UBool("offlineFirst", false), } cfg.ModuleSecret(name, globalConfig, &settings.apiKey). diff --git a/modules/uptimerobot/widget.go b/modules/uptimerobot/widget.go index 1827a1e3..bcb69d24 100644 --- a/modules/uptimerobot/widget.go +++ b/modules/uptimerobot/widget.go @@ -47,6 +47,22 @@ func (widget *Widget) Refresh() { } monitors, err := widget.getMonitors() + + if widget.settings.offlineFirst { + var tmp Monitor + var next int + for i := 0; i < len(monitors); i++ { + if monitors[i].State != 2 { + tmp = monitors[i] + for j := i; j > next; j-- { + monitors[j] = monitors[j-1] + } + monitors[next] = tmp + next++ + } + } + } + widget.monitors = monitors widget.err = err widget.SetItemCount(len(monitors)) From 21bdf18e9842a4f417e8f682f9dd1ba0008905fe Mon Sep 17 00:00:00 2001 From: Chris Cummer Date: Sun, 11 Oct 2020 20:10:57 -0700 Subject: [PATCH 5/5] Remove a redundant byte conversion Signed-off-by: Chris Cummer --- modules/uptimerobot/widget.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/uptimerobot/widget.go b/modules/uptimerobot/widget.go index bcb69d24..a905e354 100644 --- a/modules/uptimerobot/widget.go +++ b/modules/uptimerobot/widget.go @@ -152,7 +152,7 @@ type Monitor struct { func (widget *Widget) getMonitors() ([]Monitor, error) { // See: https://uptimerobot.com/api/#getMonitorsWrap - resp, err_h := http.PostForm("https://api.uptimerobot.com/v2/getMonitors", + resp, errh := http.PostForm("https://api.uptimerobot.com/v2/getMonitors", url.Values{ "api_key": {widget.settings.apiKey}, "format": {"json"}, @@ -160,18 +160,18 @@ func (widget *Widget) getMonitors() ([]Monitor, error) { }, ) - if err_h != nil { - return nil, err_h + if errh != nil { + return nil, errh } body, _ := ioutil.ReadAll(resp.Body) // First pass to read the status c := make(map[string]json.RawMessage) - err_j1 := json.Unmarshal([]byte(body), &c) + errj1 := json.Unmarshal(body, &c) - if err_j1 != nil { - return nil, err_j1 + if errj1 != nil { + return nil, errj1 } if string(c["stat"]) != `"ok"` { @@ -180,10 +180,10 @@ func (widget *Widget) getMonitors() ([]Monitor, error) { // Second pass to get the actual info var monitors []Monitor - err_j2 := json.Unmarshal(c["monitors"], &monitors) + errj2 := json.Unmarshal(c["monitors"], &monitors) - if err_j2 != nil { - return nil, err_j2 + if errj2 != nil { + return nil, errj2 } return monitors, nil