glance/internal/glance/widget-monitor.go
2025-01-17 19:09:49 +00:00

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
}