Skip to content
quarry Artifact [ba7d01d4cf]
Login

Artifact [ba7d01d4cf]

Artifact ba7d01d4cf85d40d60132cce5f35d3f4bb766580172fabf8e27305536444a060:


package web

import (
	"fmt"
	"strconv"
	"strings"
	"time"
)

// The operations page's model beyond what ops.status sends: the
// headline, the figures the strip shows, and the folds that keep the
// stores table short. Rules about what is wrong live in the op, not
// here; this only words what it answered.

// opsFindingView is one finding as ops.status answers it.
type opsFindingView struct {
	Key      string `json:"key"`
	About    string `json:"about"`
	Severity string `json:"severity"`
	Text     string `json:"text"`
	Since    string `json:"since"`
	Link     string `json:"link"`
}

// opsClosedView is the last finding that stopped being one.
type opsClosedView struct {
	Key   string `json:"key"`
	About string `json:"about"`
	Text  string `json:"text"`
	Since string `json:"since"`
	Until string `json:"until"`
	Link  string `json:"link"`
}

type opsDepView struct {
	Name    string `json:"name"`
	Why     string `json:"why"`
	Up      bool   `json:"up"`
	Since   string `json:"since"`
	DownFor string `json:"downFor"`
	Err     string `json:"err"`
	Checked string `json:"checked"`
}

type opsDiskView struct {
	Path      string `json:"path"`
	Total     uint64 `json:"total"`
	Free      uint64 `json:"free"`
	Used      uint64 `json:"used"`
	Stores    uint64 `json:"stores"`
	Indexes   uint64 `json:"indexes"`
	Analytics uint64 `json:"analytics"`
	Other     uint64 `json:"other"`
}

// Full is the disk past the point the op calls a finding.
func (d opsDiskView) Full() bool { return d.Total > 0 && float64(d.Used)/float64(d.Total) >= 0.8 }

type opsStoreView struct {
	Name         string  `json:"name"`
	State        string  `json:"state"`
	Why          string  `json:"why"`
	StoreBytes   uint64  `json:"storeBytes"`
	IndexBytes   uint64  `json:"indexBytes"`
	LastSync     string  `json:"lastSync"`
	RoundSeconds float64 `json:"roundSeconds"`
	Checkins     int     `json:"checkins"`
}

// opsRunningView is one live zit process as ops.status answers it.
type opsRunningView struct {
	Verb    string `json:"verb"`
	Repo    string `json:"repo"`
	Cause   string `json:"cause"`
	Started string `json:"started"`
	AgeMs   int64  `json:"ageMs"`
	UsualMs int64  `json:"usualMs"`
	RSS     uint64 `json:"rss"`
	Slow    bool   `json:"slow"`
}

// Address is whether the cause is a request's address, which reads as
// code, rather than a background activity, which reads as a word.
func (r opsRunningView) Address() bool {
	m, _, ok := strings.Cut(r.Cause, " ")
	return ok && m != "" && strings.ToUpper(m) == m && strings.Contains(r.Cause, "/")
}

// RunningRSS sums the resident memory of the live processes.
func (v opsView) RunningRSS() (n uint64) {
	for _, r := range v.Running {
		n += r.RSS
	}
	return n
}

// opsStatusLine is the headline and its tone: start while stores come
// up, bad or warn by the worst finding, ok when there is none.
type opsStatusLine struct {
	Tone string // start | bad | warn | ok
	Head string
}

// Status words the moment. The phase outranks findings: during startup
// the panels read as an empty forge, and "Starting: 9 of 24 stores
// ready" is the only true sentence on the page.
func (v opsView) Status() opsStatusLine {
	switch v.Phase.Name {
	case "starting":
		return opsStatusLine{Tone: "start", Head: fmt.Sprintf("Starting: %d of %d stores ready", v.Phase.Ready, v.Phase.Repos)}
	case "indexing":
		return opsStatusLine{Tone: "start", Head: "Indexing: rebuilding " + strings.Join(v.Phase.Working, ", ")}
	}
	bad, warn := 0, 0
	var first *opsFindingView
	for i := range v.Findings {
		f := &v.Findings[i]
		if f.Severity == "bad" {
			bad++
		} else {
			warn++
		}
		if first == nil {
			first = f
		}
	}
	switch {
	case bad == 1 && warn == 0, bad == 0 && warn == 1:
		tone := "warn"
		if bad == 1 {
			tone = "bad"
		}
		return opsStatusLine{Tone: tone, Head: strings.ToUpper(first.About[:1]) + first.About[1:] + ": " + first.Text}
	case bad > 0:
		return opsStatusLine{Tone: "bad", Head: fmt.Sprintf("Degraded: %d things need attention", bad+warn)}
	case warn > 0:
		return opsStatusLine{Tone: "warn", Head: fmt.Sprintf("Attention: %d things to look at", warn)}
	case v.Phase.Name == "syncing":
		return opsStatusLine{Tone: "ok", Head: "Syncing"}
	}
	return opsStatusLine{Tone: "ok", Head: "All quiet"}
}

// StartedAt is the process start as a time, zero when unknown.
func (v opsView) StartedAt() time.Time {
	t, err := time.Parse(time.RFC3339, v.Started)
	if err != nil {
		return time.Time{}
	}
	return t
}

// Uptime words how long the process has been up, "" when unknown.
func (v opsView) Uptime() string {
	t := v.StartedAt()
	if t.IsZero() {
		return ""
	}
	d := time.Since(t)
	switch {
	case d < time.Minute:
		return fmt.Sprintf("%ds", int(d.Seconds()))
	case d < time.Hour:
		return fmt.Sprintf("%dm", int(d.Minutes()))
	case d < 48*time.Hour:
		return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60)
	}
	return fmt.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24)
}

// RendersPerMinute is the last whole minute's count: the current
// minute is still filling and would read low.
func (v opsView) RendersPerMinute() int {
	r := v.Signals.Renders
	switch len(r) {
	case 0:
		return 0
	case 1:
		return r[0]
	}
	return r[len(r)-2]
}

// ErrorsLastHour sums the error signal; without a durable journal it
// falls back to the ring's total since start.
func (v opsView) ErrorsLastHour() int {
	if v.Signals.Errors == nil {
		return v.ErrorTotal
	}
	n := 0
	for _, x := range v.Signals.Errors {
		n += x
	}
	return n
}

// StoreBound is the store subprocess lane's slot count, 0 if unknown.
func (v opsView) StoreBound() int {
	for _, l := range v.Lanes {
		if l.Name == "store subprocesses" {
			return l.Bound
		}
	}
	return 0
}

// StoreBytes and IndexBytes sum the stores table.
func (v opsView) StoreBytes() (n uint64) {
	for _, s := range v.Stores {
		n += s.StoreBytes
	}
	return n
}

func (v opsView) IndexBytes() (n uint64) {
	for _, s := range v.Stores {
		n += s.IndexBytes
	}
	return n
}

// storesListed is how many current stores the table shows before the
// rest fold into one line.
const storesListed = 6

// StoresShown is every store needing attention and the first few that
// are current; StoresFolded counts the rest.
func (v opsView) StoresShown() []opsStoreView {
	var out []opsStoreView
	current := 0
	for _, s := range v.Stores {
		if s.State == "current" {
			current++
			if current > storesListed {
				continue
			}
		}
		out = append(out, s)
	}
	return out
}

func (v opsView) StoresFolded() int {
	current := 0
	for _, s := range v.Stores {
		if s.State == "current" {
			current++
		}
	}
	return max(0, current-storesListed)
}

// RenderScale is the slowest render listed, which every row's bar is
// drawn against; at least the budget, so a quiet list still shows
// where the budget falls.
func (v opsView) RenderScale() int64 {
	scale := v.BudgetMs
	for _, r := range v.Renders {
		if r.ElapsedMs > scale {
			scale = r.ElapsedMs
		}
	}
	return max(scale, 1)
}

// msWord is a duration in the unit a reader thinks in: 41 ms, 1.32 s,
// 91.2 s, 1 m 28 s.
func msWord(ms int64) string {
	switch {
	case ms < 1000:
		return strconv.FormatInt(ms, 10) + " ms"
	case ms < 10_000:
		return fmt.Sprintf("%.2f s", float64(ms)/1000)
	case ms < 60_000:
		return fmt.Sprintf("%.1f s", float64(ms)/1000)
	}
	m, s := ms/60_000, (ms%60_000)/1000
	if s == 0 {
		return fmt.Sprintf("%d m", m)
	}
	return fmt.Sprintf("%d m %d s", m, s)
}

// spanMs is a long span in days, hours and minutes: an event stream's
// oldest message is "1d 17h" old, not "2460m".
func spanMs(ms int64) string {
	d := time.Duration(ms) * time.Millisecond
	switch {
	case d < time.Minute:
		return fmt.Sprintf("%ds", int(d.Seconds()))
	case d < time.Hour:
		return fmt.Sprintf("%dm", int(d.Minutes()))
	case d < 24*time.Hour:
		return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60)
	}
	return fmt.Sprintf("%dd %dh", int(d.Hours()/24), int(d.Hours())%24)
}

// pctBudget is a duration as a percentage of the budget, capped so a
// bar never leaves its track.
func pctBudget(ms, budget int64) string {
	if budget <= 0 {
		return "0"
	}
	return fmt.Sprintf("%.1f", min(100, float64(ms)/float64(budget)*100))
}

func pctOf64(n, total int64) string {
	if total <= 0 || n <= 0 {
		return "0"
	}
	return fmt.Sprintf("%.1f", min(100, float64(n)/float64(total)*100))
}

// findingGo is the link text for a finding's destination.
func findingGo(about string) string {
	switch about {
	case "lane":
		return "lanes ↓"
	case "slow":
		return "this route →"
	case "errors":
		return "exceptions →"
	case "store":
		return "its store →"
	case "history":
		return "history →"
	case "dependency":
		return "fleet →"
	case "disk":
		return "disk ↓"
	case "memory":
		return "memory ↓"
	}
	return "open →"
}

// storeTone is the label colour for an index state.
func storeTone(state string) string {
	switch state {
	case "current":
		return "ov-ok"
	case "stalled", "failing":
		return "ov-bad"
	case "behind", "gaps":
		return "ov-warn"
	}
	return "ov-mute"
}