| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- // 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
- TimeSinceLastStart time.Duration
- TimeSinceLastEnd 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.Error != "" {
- vars.UnhealthyProbes++
- }
- s := probeStatus{ProbeInfo: info}
- if !info.Start.IsZero() {
- s.TimeSinceLastStart = time.Since(info.Start).Truncate(time.Second)
- }
- if !info.End.IsZero() {
- s.TimeSinceLastEnd = time.Since(info.End).Truncate(time.Second)
- }
- 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
- }
|