mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
Healthcecks module
Healthchecks.io is a service for monitoring cron jobs and similar periodic processes. Hosted: https://healthchecks.io/ API-Doc: https://healthchecks.io/docs/api/ GitHub: https://github.com/healthchecks/healthchecks This module can be used both with hosted and self-hosted healthchecks.
This commit is contained in:
parent
9a703dd78b
commit
2341446376
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/wtfutil/wtf/modules/grafana"
|
"github.com/wtfutil/wtf/modules/grafana"
|
||||||
"github.com/wtfutil/wtf/modules/gspreadsheets"
|
"github.com/wtfutil/wtf/modules/gspreadsheets"
|
||||||
"github.com/wtfutil/wtf/modules/hackernews"
|
"github.com/wtfutil/wtf/modules/hackernews"
|
||||||
|
"github.com/wtfutil/wtf/modules/healthchecks"
|
||||||
"github.com/wtfutil/wtf/modules/hibp"
|
"github.com/wtfutil/wtf/modules/hibp"
|
||||||
"github.com/wtfutil/wtf/modules/ipaddresses/ipapi"
|
"github.com/wtfutil/wtf/modules/ipaddresses/ipapi"
|
||||||
"github.com/wtfutil/wtf/modules/ipaddresses/ipinfo"
|
"github.com/wtfutil/wtf/modules/ipaddresses/ipinfo"
|
||||||
@ -201,6 +202,9 @@ func MakeWidget(
|
|||||||
case "hackernews":
|
case "hackernews":
|
||||||
settings := hackernews.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := hackernews.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = hackernews.NewWidget(tviewApp, pages, settings)
|
widget = hackernews.NewWidget(tviewApp, pages, settings)
|
||||||
|
case "healthchecks":
|
||||||
|
settings := healthchecks.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
|
widget = healthchecks.NewWidget(tviewApp, pages, settings)
|
||||||
case "hibp":
|
case "hibp":
|
||||||
settings := hibp.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := hibp.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = hibp.NewWidget(tviewApp, settings)
|
widget = hibp.NewWidget(tviewApp, settings)
|
||||||
|
6
modules/healthchecks/keyboard.go
Normal file
6
modules/healthchecks/keyboard.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package healthchecks
|
||||||
|
|
||||||
|
func (widget *Widget) initializeKeyboardControls() {
|
||||||
|
widget.InitializeHelpTextKeyboardControl(widget.ShowHelp)
|
||||||
|
widget.InitializeRefreshKeyboardControl(widget.Refresh)
|
||||||
|
}
|
38
modules/healthchecks/settings.go
Normal file
38
modules/healthchecks/settings.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package healthchecks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/olebedev/config"
|
||||||
|
"github.com/wtfutil/wtf/cfg"
|
||||||
|
"github.com/wtfutil/wtf/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultFocusable = true
|
||||||
|
defaultTitle = "Healthchecks.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
*cfg.Common
|
||||||
|
|
||||||
|
apiKey string `help:"An healthchecks API key." optional:"false"`
|
||||||
|
apiURL string `help:"Base URL for API" optional:"true"`
|
||||||
|
tags []string `help:"Filters the checks and returns only the checks that are tagged with the specified value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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_HEALTHCHECKS_APIKEY")),
|
||||||
|
apiURL: ymlConfig.UString("apiURL", "https://hc-ping.com/"),
|
||||||
|
tags: utils.ToStrs(ymlConfig.UList("tags")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.ModuleSecret(name, globalConfig, &settings.apiKey).
|
||||||
|
Service("https://hc-ping.com/").Load()
|
||||||
|
|
||||||
|
return &settings
|
||||||
|
}
|
184
modules/healthchecks/widget.go
Normal file
184
modules/healthchecks/widget.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package healthchecks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rivo/tview"
|
||||||
|
"github.com/wtfutil/wtf/utils"
|
||||||
|
"github.com/wtfutil/wtf/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userAgent = "WTFUtil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Widget struct {
|
||||||
|
view.ScrollableWidget
|
||||||
|
checks []Checks
|
||||||
|
settings *Settings
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Health struct {
|
||||||
|
Checks []Checks `json:"checks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Checks struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Grace int `json:"grace"`
|
||||||
|
NPings int `json:"n_pings"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LastPing time.Time `json:"last_ping"`
|
||||||
|
NextPing time.Time `json:"next_ping"`
|
||||||
|
ManualResume bool `json:"manual_resume"`
|
||||||
|
Methods string `json:"methods"`
|
||||||
|
PingURL string `json:"ping_url"`
|
||||||
|
UpdateURL string `json:"update_url"`
|
||||||
|
PauseURL string `json:"pause_url"`
|
||||||
|
Channels string `json:"channels"`
|
||||||
|
Timeout int `json:"timeout,omitempty"`
|
||||||
|
Schedule string `json:"schedule,omitempty"`
|
||||||
|
Tz string `json:"tz,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWidget(tviewApp *tview.Application, pages *tview.Pages, settings *Settings) *Widget {
|
||||||
|
widget := &Widget{
|
||||||
|
ScrollableWidget: view.NewScrollableWidget(tviewApp, pages, settings.Common),
|
||||||
|
settings: settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.SetRenderFunction(widget.Render)
|
||||||
|
widget.initializeKeyboardControls()
|
||||||
|
|
||||||
|
return widget
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------- Exported Functions -------------------- */
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------- Unexported Functions -------------------- */
|
||||||
|
|
||||||
|
func (widget *Widget) content() (string, string, bool) {
|
||||||
|
numUp := 0
|
||||||
|
for _, check := range widget.checks {
|
||||||
|
if check.Status == "up" {
|
||||||
|
numUp++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title := fmt.Sprintf("Healthchecks (%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 []Checks) string {
|
||||||
|
var str string
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
prefix := ""
|
||||||
|
|
||||||
|
switch check.Status {
|
||||||
|
case "up":
|
||||||
|
prefix += "[green] + "
|
||||||
|
case "down":
|
||||||
|
prefix += "[red] - "
|
||||||
|
default:
|
||||||
|
prefix += "[yellow] ~ "
|
||||||
|
}
|
||||||
|
|
||||||
|
str += fmt.Sprintf(`%s%s [gray](%s|%d)[white]%s`,
|
||||||
|
prefix,
|
||||||
|
check.Name,
|
||||||
|
timeSincePing(check.LastPing),
|
||||||
|
check.NPings,
|
||||||
|
"\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, tags []string) (string, error) {
|
||||||
|
u, err := url.Parse(baseurl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
u.Path = path
|
||||||
|
q := u.Query()
|
||||||
|
// If we have several tags
|
||||||
|
if len(tags) > 0 {
|
||||||
|
for _, tag := range tags {
|
||||||
|
q.Add("tag", tag)
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Widget) getExistingChecks() ([]Checks, error) {
|
||||||
|
// See: https://healthchecks.io/docs/api/#list-checks
|
||||||
|
u, err := makeURL(widget.settings.apiURL, "/api/v1/checks/", widget.settings.tags)
|
||||||
|
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 resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf(resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
var health Health
|
||||||
|
err = utils.ParseJSON(&health, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return health.Checks, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user