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..878e4fa5 --- /dev/null +++ b/modules/uptimerobot/settings.go @@ -0,0 +1,37 @@ +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"` + offlineFirst bool `help:"Display offline monitors at the top." 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"), + offlineFirst: ymlConfig.UBool("offlineFirst", false), + } + + 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..a905e354 --- /dev/null +++ b/modules/uptimerobot/widget.go @@ -0,0 +1,190 @@ +package uptimerobot + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "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() + + 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)) + + 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] + " + case 8: + case 9: + prefix += "[red] - " + default: + prefix += "[yellow] ~ " + } + + str += fmt.Sprintf(`%s%s [gray](%s)[white] +`, + prefix, + monitor.Name, + 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 + 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, errh := http.PostForm("https://api.uptimerobot.com/v2/getMonitors", + url.Values{ + "api_key": {widget.settings.apiKey}, + "format": {"json"}, + "custom_uptime_ratios": {widget.settings.uptimePeriods}, + }, + ) + + if errh != nil { + return nil, errh + } + + body, _ := ioutil.ReadAll(resp.Body) + + // First pass to read the status + c := make(map[string]json.RawMessage) + errj1 := json.Unmarshal(body, &c) + + if errj1 != nil { + return nil, errj1 + } + + if string(c["stat"]) != `"ok"` { + return nil, errors.New(string(body)) + } + + // Second pass to get the actual info + var monitors []Monitor + errj2 := json.Unmarshal(c["monitors"], &monitors) + + if errj2 != nil { + return nil, errj2 + } + + return monitors, nil +}