182 lines
4.2 KiB
Go
182 lines
4.2 KiB
Go
package glance
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"html/template"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
monitorWidgetTemplate = mustParseTemplate("monitor.html", "widget-base.html")
|
|
monitorWidgetCompactTemplate = mustParseTemplate("monitor-compact.html", "widget-base.html")
|
|
)
|
|
|
|
type monitorWidget struct {
|
|
widgetBase `yaml:",inline"`
|
|
Sites []struct {
|
|
*SiteStatusRequest `yaml:",inline"`
|
|
Status *siteStatus `yaml:"-"`
|
|
URL string `yaml:"-"`
|
|
ErrorURL string `yaml:"error-url"`
|
|
Title string `yaml:"title"`
|
|
Icon customIconField `yaml:"icon"`
|
|
SameTab bool `yaml:"same-tab"`
|
|
StatusText string `yaml:"-"`
|
|
StatusStyle string `yaml:"-"`
|
|
AltStatusCodes []int `yaml:"alt-status-codes"`
|
|
} `yaml:"sites"`
|
|
Style string `yaml:"style"`
|
|
ShowFailingOnly bool `yaml:"show-failing-only"`
|
|
HasFailing bool `yaml:"-"`
|
|
}
|
|
|
|
func (widget *monitorWidget) initialize() error {
|
|
widget.withTitle("Monitor").withCacheDuration(5 * time.Minute)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (widget *monitorWidget) update(ctx context.Context) {
|
|
requests := make([]*SiteStatusRequest, len(widget.Sites))
|
|
|
|
for i := range widget.Sites {
|
|
requests[i] = widget.Sites[i].SiteStatusRequest
|
|
}
|
|
|
|
statuses, err := fetchStatusForSites(requests)
|
|
|
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
|
return
|
|
}
|
|
|
|
widget.HasFailing = false
|
|
|
|
for i := range widget.Sites {
|
|
site := &widget.Sites[i]
|
|
status := &statuses[i]
|
|
site.Status = status
|
|
|
|
if !slices.Contains(site.AltStatusCodes, status.Code) && (status.Code >= 400 || status.Error != nil) {
|
|
widget.HasFailing = true
|
|
}
|
|
|
|
if status.Error != nil && site.ErrorURL != "" {
|
|
site.URL = site.ErrorURL
|
|
} else {
|
|
site.URL = site.DefaultURL
|
|
}
|
|
|
|
site.StatusText = statusCodeToText(status.Code, site.AltStatusCodes)
|
|
site.StatusStyle = statusCodeToStyle(status.Code, site.AltStatusCodes)
|
|
}
|
|
}
|
|
|
|
func (widget *monitorWidget) Render() template.HTML {
|
|
if widget.Style == "compact" {
|
|
return widget.renderTemplate(widget, monitorWidgetCompactTemplate)
|
|
}
|
|
|
|
return widget.renderTemplate(widget, monitorWidgetTemplate)
|
|
}
|
|
|
|
func statusCodeToText(status int, altStatusCodes []int) string {
|
|
if status == 200 || slices.Contains(altStatusCodes, status) {
|
|
return "OK"
|
|
}
|
|
if status == 404 {
|
|
return "Not Found"
|
|
}
|
|
if status == 403 {
|
|
return "Forbidden"
|
|
}
|
|
if status == 401 {
|
|
return "Unauthorized"
|
|
}
|
|
if status >= 500 {
|
|
return "Server Error"
|
|
}
|
|
if status >= 400 {
|
|
return "Client Error"
|
|
}
|
|
|
|
return strconv.Itoa(status)
|
|
}
|
|
|
|
func statusCodeToStyle(status int, altStatusCodes []int) string {
|
|
if status == 200 || slices.Contains(altStatusCodes, status) {
|
|
return "ok"
|
|
}
|
|
|
|
return "error"
|
|
}
|
|
|
|
type SiteStatusRequest struct {
|
|
DefaultURL string `yaml:"url"`
|
|
CheckURL string `yaml:"check-url"`
|
|
AllowInsecure bool `yaml:"allow-insecure"`
|
|
}
|
|
|
|
type siteStatus struct {
|
|
Code int
|
|
TimedOut bool
|
|
ResponseTime time.Duration
|
|
Error error
|
|
}
|
|
|
|
func fetchSiteStatusTask(statusRequest *SiteStatusRequest) (siteStatus, error) {
|
|
var url string
|
|
if statusRequest.CheckURL != "" {
|
|
url = statusRequest.CheckURL
|
|
} else {
|
|
url = statusRequest.DefaultURL
|
|
}
|
|
request, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return siteStatus{
|
|
Error: err,
|
|
}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
|
defer cancel()
|
|
request = request.WithContext(ctx)
|
|
requestSentAt := time.Now()
|
|
var response *http.Response
|
|
|
|
if !statusRequest.AllowInsecure {
|
|
response, err = defaultHTTPClient.Do(request)
|
|
} else {
|
|
response, err = defaultInsecureHTTPClient.Do(request)
|
|
}
|
|
|
|
status := siteStatus{ResponseTime: time.Since(requestSentAt)}
|
|
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
status.TimedOut = true
|
|
}
|
|
|
|
status.Error = err
|
|
return status, nil
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
status.Code = response.StatusCode
|
|
|
|
return status, nil
|
|
}
|
|
|
|
func fetchStatusForSites(requests []*SiteStatusRequest) ([]siteStatus, error) {
|
|
job := newJob(fetchSiteStatusTask, requests).withWorkers(20)
|
|
results, _, err := workerPoolDo(job)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return results, nil
|
|
}
|