Skip to content

Commit

Permalink
Add module for Updown.io (wtfutil#1097)
Browse files Browse the repository at this point in the history
  • Loading branch information
hjr265 authored Aug 8, 2021
1 parent 4f8a8b9 commit 0fa28f9
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
4 changes: 4 additions & 0 deletions app/widget_maker.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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/updown"
"github.com/wtfutil/wtf/modules/uptimerobot"
"github.com/wtfutil/wtf/modules/victorops"
"github.com/wtfutil/wtf/modules/weatherservices/arpansagovau"
Expand Down Expand Up @@ -315,6 +316,9 @@ func MakeWidget(
case "twitterstats":
settings := twitterstats.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = twitterstats.NewWidget(tviewApp, pages, settings)
case "updown":
settings := updown.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = updown.NewWidget(tviewApp, pages, settings)
case "uptimerobot":
settings := uptimerobot.NewSettingsFromYAML(moduleName, moduleConfig, config)
widget = uptimerobot.NewWidget(tviewApp, pages, settings)
Expand Down
6 changes: 6 additions & 0 deletions modules/updown/keyboard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package updown

func (widget *Widget) initializeKeyboardControls() {
widget.InitializeHelpTextKeyboardControl(widget.ShowHelp)
widget.InitializeRefreshKeyboardControl(widget.Refresh)
}
35 changes: 35 additions & 0 deletions modules/updown/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package updown

import (
"os"

"github.com/olebedev/config"
"github.com/wtfutil/wtf/cfg"
"github.com/wtfutil/wtf/utils"
)

const (
defaultFocusable = true
defaultTitle = "Updown.io"
)

type Settings struct {
*cfg.Common

apiKey string `help:"An Updown API key." optional:"false"`
tokens []string `help:"Filters the checks and returns only the checks with the specified tokens"`
}

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_UPDOWN_APIKEY")),
tokens: utils.ToStrs(ymlConfig.UList("tokens")),
}

cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()

return &settings
}
202 changes: 202 additions & 0 deletions modules/updown/widget.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package updown

import (
"fmt"
"net/http"
"net/url"
"time"

"github.com/rivo/tview"
"github.com/wtfutil/wtf/utils"
"github.com/wtfutil/wtf/view"
)

const (
userAgent = "WTFUtil"

apiURLBase = "https://updown.io"
)

type Widget struct {
view.ScrollableWidget
checks []Check
settings *Settings
tokenSet map[string]struct{}
err error
}

// Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L17
type Check struct {
Token string `json:"token,omitempty"`
URL string `json:"url,omitempty"`
Alias string `json:"alias,omitempty"`
LastStatus int `json:"last_status,omitempty"`
Uptime float64 `json:"uptime,omitempty"`
Down bool `json:"down"`
DownSince string `json:"down_since,omitempty"`
Error string `json:"error,omitempty"`
Period int `json:"period,omitempty"`
Apdex float64 `json:"apdex_t,omitempty"`
Enabled bool `json:"enabled"`
Published bool `json:"published"`
LastCheckAt time.Time `json:"last_check_at,omitempty"`
NextCheckAt time.Time `json:"next_check_at,omitempty"`
FaviconURL string `json:"favicon_url,omitempty"`
SSL SSL `json:"ssl,omitempty"`
StringMatch string `json:"string_match,omitempty"`
MuteUntil string `json:"mute_until,omitempty"`
DisabledLocations []string `json:"disabled_locations,omitempty"`
CustomHeaders map[string]string `json:"custom_headers,omitempty"`
}

// Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L10
type SSL struct {
TestedAt string `json:"tested_at,omitempty"`
Valid bool `json:"valid,omitempty"`
Error string `json:"error,omitempty"`
}

func NewWidget(tviewApp *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
widget := &Widget{
ScrollableWidget: view.NewScrollableWidget(tviewApp, pages, settings.Common),
settings: settings,
tokenSet: make(map[string]struct{}),
}

for _, t := range settings.tokens {
widget.tokenSet[t] = struct{}{}
}

widget.SetRenderFunction(widget.Render)
widget.initializeKeyboardControls()

return widget
}

func (widget *Widget) Refresh() {
if widget.Disabled() {
return
}

checks, err := widget.getExistingChecks()
widget.checks = checks
widget.err = err
widget.SetItemCount(len(checks))
widget.Render()
}

// Render sets up the widget data for redrawing to the screen
func (widget *Widget) Render() {
widget.Redraw(widget.content)
}

func (widget *Widget) content() (string, string, bool) {
numUp := 0
for _, check := range widget.checks {
if !check.Down {
numUp++
}
}

title := fmt.Sprintf("Updown (%d/%d)", numUp, len(widget.checks))

if widget.err != nil {
return title, widget.err.Error(), true
}

if widget.checks == nil {
return title, "No checks to display", false
}

str := widget.contentFrom(widget.checks)

return title, str, false
}

func (widget *Widget) contentFrom(checks []Check) string {
var str string

for _, check := range checks {
prefix := ""

if !check.Enabled {
prefix += "[yellow] ~ "
} else if check.Down {
prefix += "[red] - "
} else {
prefix += "[green] + "
}

str += fmt.Sprintf(`%s%s [gray](%0.2f|%s)[white]%s`,
prefix,
check.Alias,
check.Uptime,
timeSincePing(check.LastCheckAt),
"\n",
)
}

return str
}

func timeSincePing(ts time.Time) string {
dur := time.Since(ts)
return dur.Truncate(time.Second).String()
}

func makeURL(baseurl string, path string) (string, error) {
u, err := url.Parse(baseurl)
if err != nil {
return "", err
}
u.Path = path
return u.String(), nil
}

func filterChecks(checks []Check, tokenSet map[string]struct{}) []Check {
j := 0
for i := 0; i < len(checks); i++ {
if _, ok := tokenSet[checks[i].Token]; ok {
checks[j] = checks[i]
j++
}
}
return checks[:j]
}

func (widget *Widget) getExistingChecks() ([]Check, error) {
// See: https://updown.io/api#rest
u, err := makeURL(apiURLBase, "/api/checks")
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-API-KEY", widget.settings.apiKey)
resp, err := http.DefaultClient.Do(req)

if err != nil {
return nil, err
}

if resp.StatusCode != 200 {
return nil, fmt.Errorf(resp.Status)
}

defer func() { _ = resp.Body.Close() }()

var checks []Check
err = utils.ParseJSON(&checks, resp.Body)
if err != nil {
return nil, err
}

if len(widget.tokenSet) > 0 {
checks = filterChecks(checks, widget.tokenSet)
}

return checks, nil
}

0 comments on commit 0fa28f9

Please sign in to comment.