diff --git a/prober/status.go b/prober/status.go new file mode 100644 index 000000000..034c5302d --- /dev/null +++ b/prober/status.go @@ -0,0 +1,124 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package prober + +import ( + "embed" + "fmt" + "html/template" + "net/http" + "strings" + "time" + + "tailscale.com/tsweb" + "tailscale.com/util/mak" +) + +//go:embed status.html +var statusFiles embed.FS +var statusTpl = template.Must(template.ParseFS(statusFiles, "status.html")) + +type statusHandlerOpt func(*statusHandlerParams) +type statusHandlerParams struct { + title string + + pageLinks map[string]string + probeLinks map[string]string +} + +// WithTitle sets the title of the status page. +func WithTitle(title string) statusHandlerOpt { + return func(opts *statusHandlerParams) { + opts.title = title + } +} + +// WithPageLink adds a top-level link to the status page. +func WithPageLink(text, url string) statusHandlerOpt { + return func(opts *statusHandlerParams) { + mak.Set(&opts.pageLinks, text, url) + } +} + +// WithProbeLink adds a link to each probe on the status page. +// The textTpl and urlTpl are Go templates that will be rendered +// with the respective ProbeInfo struct as the data. +func WithProbeLink(textTpl, urlTpl string) statusHandlerOpt { + return func(opts *statusHandlerParams) { + mak.Set(&opts.probeLinks, textTpl, urlTpl) + } +} + +// StatusHandler is a handler for the probe overview HTTP endpoint. +// It shows a list of probes and their current status. +func (p *Prober) StatusHandler(opts ...statusHandlerOpt) tsweb.ReturnHandlerFunc { + params := &statusHandlerParams{ + title: "Prober Status", + } + for _, opt := range opts { + opt(params) + } + return func(w http.ResponseWriter, r *http.Request) error { + type probeStatus struct { + ProbeInfo + TimeSinceLast time.Duration + Links map[string]template.URL + } + vars := struct { + Title string + Links map[string]template.URL + TotalProbes int64 + UnhealthyProbes int64 + Probes map[string]probeStatus + }{ + Title: params.title, + } + + for text, url := range params.pageLinks { + mak.Set(&vars.Links, text, template.URL(url)) + } + + for name, info := range p.ProbeInfo() { + vars.TotalProbes++ + if !info.Result { + vars.UnhealthyProbes++ + } + s := probeStatus{ProbeInfo: info} + if !info.End.IsZero() { + s.TimeSinceLast = time.Since(info.End) + } + for textTpl, urlTpl := range params.probeLinks { + text, err := renderTemplate(textTpl, info) + if err != nil { + return tsweb.Error(500, err.Error(), err) + } + url, err := renderTemplate(urlTpl, info) + if err != nil { + return tsweb.Error(500, err.Error(), err) + } + mak.Set(&s.Links, text, template.URL(url)) + } + mak.Set(&vars.Probes, name, s) + } + + if err := statusTpl.ExecuteTemplate(w, "status", vars); err != nil { + return tsweb.HTTPError{Code: 500, Err: err, Msg: "error rendering status page"} + } + return nil + } +} + +// renderTemplate renders the given Go template with the provided data +// and returns the result as a string. +func renderTemplate(tpl string, data any) (string, error) { + t, err := template.New("").Parse(tpl) + if err != nil { + return "", fmt.Errorf("error parsing template %q: %w", tpl, err) + } + var buf strings.Builder + if err := t.ExecuteTemplate(&buf, "", data); err != nil { + return "", fmt.Errorf("error rendering template %q with data %v: %w", tpl, data, err) + } + return buf.String(), nil +} diff --git a/prober/status.html b/prober/status.html new file mode 100644 index 000000000..eecce1463 --- /dev/null +++ b/prober/status.html @@ -0,0 +1,132 @@ +{{define "status"}} + +
Name | +Class & Labels | +Interval | +Result | +Success | +Latency | +Error | +
---|---|---|---|---|---|---|
+ {{$name}}
+ {{range $text, $url := $probeInfo.Links}}
+ + + {{end}} + |
+ {{$probeInfo.Class}} +
+ {{range $label, $value := $probeInfo.Labels}}
+ {{$label}}={{$value}}
+ + {{end}} + |
+ {{$probeInfo.Interval}} | +
+ {{if $probeInfo.TimeSinceLast}}
+ {{$probeInfo.TimeSinceLast.String}} + {{$probeInfo.End}} + {{else}} + Never + {{end}} + |
+
+ {{if $probeInfo.Result}}
+ {{$probeInfo.Result}}
+ {{else}}
+ {{$probeInfo.Result}}
+ {{end}} + Recent: {{$probeInfo.RecentResults}}
+ Mean: {{$probeInfo.RecentSuccessRatio}}
+ |
+
+ {{$probeInfo.Latency.String}}
+ Recent: {{$probeInfo.RecentLatencies}}
+ Median: {{$probeInfo.RecentMedianLatency}}
+ |
+ {{$probeInfo.Error}} | +