github.com/wtfutil/wtf@v0.43.0/modules/updown/widget.go (about)

     1  package updown
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"time"
     8  
     9  	"github.com/rivo/tview"
    10  	"github.com/wtfutil/wtf/utils"
    11  	"github.com/wtfutil/wtf/view"
    12  )
    13  
    14  const (
    15  	userAgent = "WTFUtil"
    16  
    17  	apiURLBase = "https://updown.io"
    18  )
    19  
    20  type Widget struct {
    21  	view.ScrollableWidget
    22  	checks   []Check
    23  	settings *Settings
    24  	tokenSet map[string]struct{}
    25  	err      error
    26  }
    27  
    28  // Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L17
    29  type Check struct {
    30  	Token             string            `json:"token,omitempty"`
    31  	URL               string            `json:"url,omitempty"`
    32  	Alias             string            `json:"alias,omitempty"`
    33  	LastStatus        int               `json:"last_status,omitempty"`
    34  	Uptime            float64           `json:"uptime,omitempty"`
    35  	Down              bool              `json:"down"`
    36  	DownSince         string            `json:"down_since,omitempty"`
    37  	Error             string            `json:"error,omitempty"`
    38  	Period            int               `json:"period,omitempty"`
    39  	Apdex             float64           `json:"apdex_t,omitempty"`
    40  	Enabled           bool              `json:"enabled"`
    41  	Published         bool              `json:"published"`
    42  	LastCheckAt       time.Time         `json:"last_check_at,omitempty"`
    43  	NextCheckAt       time.Time         `json:"next_check_at,omitempty"`
    44  	FaviconURL        string            `json:"favicon_url,omitempty"`
    45  	SSL               SSL               `json:"ssl,omitempty"`
    46  	StringMatch       string            `json:"string_match,omitempty"`
    47  	MuteUntil         string            `json:"mute_until,omitempty"`
    48  	DisabledLocations []string          `json:"disabled_locations,omitempty"`
    49  	CustomHeaders     map[string]string `json:"custom_headers,omitempty"`
    50  }
    51  
    52  // Taken from https://github.com/AntoineAugusti/updown/blob/d590ab97f115302c73ecf21647909d8fd06ed6ac/checks.go#L10
    53  type SSL struct {
    54  	TestedAt string `json:"tested_at,omitempty"`
    55  	Valid    bool   `json:"valid,omitempty"`
    56  	Error    string `json:"error,omitempty"`
    57  }
    58  
    59  func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {
    60  	widget := &Widget{
    61  		ScrollableWidget: view.NewScrollableWidget(tviewApp, redrawChan, pages, settings.Common),
    62  		settings:         settings,
    63  		tokenSet:         make(map[string]struct{}),
    64  	}
    65  
    66  	for _, t := range settings.tokens {
    67  		widget.tokenSet[t] = struct{}{}
    68  	}
    69  
    70  	widget.SetRenderFunction(widget.Render)
    71  	widget.initializeKeyboardControls()
    72  
    73  	return widget
    74  }
    75  
    76  func (widget *Widget) Refresh() {
    77  	if widget.Disabled() {
    78  		return
    79  	}
    80  
    81  	checks, err := widget.getExistingChecks()
    82  	widget.checks = checks
    83  	widget.err = err
    84  	widget.SetItemCount(len(checks))
    85  	widget.Render()
    86  }
    87  
    88  // Render sets up the widget data for redrawing to the screen
    89  func (widget *Widget) Render() {
    90  	widget.Redraw(widget.content)
    91  }
    92  
    93  func (widget *Widget) content() (string, string, bool) {
    94  	numUp := 0
    95  	for _, check := range widget.checks {
    96  		if !check.Down {
    97  			numUp++
    98  		}
    99  	}
   100  
   101  	title := fmt.Sprintf("Updown (%d/%d)", numUp, len(widget.checks))
   102  
   103  	if widget.err != nil {
   104  		return title, widget.err.Error(), true
   105  	}
   106  
   107  	if widget.checks == nil {
   108  		return title, "No checks to display", false
   109  	}
   110  
   111  	str := widget.contentFrom(widget.checks)
   112  
   113  	return title, str, false
   114  }
   115  
   116  func (widget *Widget) contentFrom(checks []Check) string {
   117  	var str string
   118  
   119  	for _, check := range checks {
   120  		prefix := ""
   121  
   122  		if !check.Enabled {
   123  			prefix += "[yellow] ~ "
   124  		} else if check.Down {
   125  			prefix += "[red] - "
   126  		} else {
   127  			prefix += "[green] + "
   128  		}
   129  
   130  		str += fmt.Sprintf(`%s%s [gray](%0.2f|%s)[white]%s`,
   131  			prefix,
   132  			check.Alias,
   133  			check.Uptime,
   134  			timeSincePing(check.LastCheckAt),
   135  			"\n",
   136  		)
   137  	}
   138  
   139  	return str
   140  }
   141  
   142  func timeSincePing(ts time.Time) string {
   143  	dur := time.Since(ts)
   144  	return dur.Truncate(time.Second).String()
   145  }
   146  
   147  func makeURL(baseurl string, path string) (string, error) {
   148  	u, err := url.Parse(baseurl)
   149  	if err != nil {
   150  		return "", err
   151  	}
   152  	u.Path = path
   153  	return u.String(), nil
   154  }
   155  
   156  func filterChecks(checks []Check, tokenSet map[string]struct{}) []Check {
   157  	j := 0
   158  	for i := 0; i < len(checks); i++ {
   159  		if _, ok := tokenSet[checks[i].Token]; ok {
   160  			checks[j] = checks[i]
   161  			j++
   162  		}
   163  	}
   164  	return checks[:j]
   165  }
   166  
   167  func (widget *Widget) getExistingChecks() ([]Check, error) {
   168  	// See: https://updown.io/api#rest
   169  	u, err := makeURL(apiURLBase, "/api/checks")
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	req, err := http.NewRequest("GET", u, http.NoBody)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	req.Header.Set("User-Agent", userAgent)
   178  	req.Header.Set("X-API-KEY", widget.settings.apiKey)
   179  	resp, err := http.DefaultClient.Do(req)
   180  
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	if resp.StatusCode != 200 {
   186  		return nil, fmt.Errorf(resp.Status)
   187  	}
   188  
   189  	defer func() { _ = resp.Body.Close() }()
   190  
   191  	var checks []Check
   192  	err = utils.ParseJSON(&checks, resp.Body)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	if len(widget.tokenSet) > 0 {
   198  		checks = filterChecks(checks, widget.tokenSet)
   199  	}
   200  
   201  	return checks, nil
   202  }