290 lines
9.9 KiB
Go
290 lines
9.9 KiB
Go
// Package stats zählt die Kennzahlen aller Workspaces für die Systemseite
|
|
// zusammen (AUSBAUSTUFEN.md 5.16): Bestand, Platz, Stand des Schemas und die
|
|
// Aktivität der letzten 30 Tage.
|
|
//
|
|
// # Warum nicht bei jedem Seitenaufruf?
|
|
//
|
|
// Für die Zahlen fragt Munin jeden Workspace einzeln ab — bei 40 Workspaces
|
|
// rund 300 Abfragen. Das ist kein Problem, wenn es selten geschieht, aber
|
|
// unnötig bei jedem Neuladen der Seite. Der Sammler rechnet deshalb höchstens
|
|
// alle MinInterval neu und gibt dazwischen das gemerkte Ergebnis zurück.
|
|
// Die Seite nennt den Stand ("Stand 14:05").
|
|
//
|
|
// Gerechnet wird beim Aufruf der Seite, nicht in einer eigenen Goroutine: Wer
|
|
// die Seite nie öffnet, kostet nichts, und es gibt keinen Hintergrundlauf,
|
|
// der beim Beenden aufgeräumt werden müsste.
|
|
package stats
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"munin/internal/store"
|
|
"munin/internal/workspace"
|
|
)
|
|
|
|
// Days ist der Zeitraum der Aktivität. Derselbe wie in der Nutzung eines
|
|
// Projekts (store.UsageWindow) — und nicht länger, weil das Suchprotokoll
|
|
// nach 30 Tagen aufgeräumt wird. Ältere Tage stünden sonst ohne Suchen da.
|
|
const Days = store.UsageWindowDays
|
|
|
|
// MinInterval ist die Zeit, die ein Ergebnis gilt, bevor neu gerechnet
|
|
// wird.
|
|
const MinInterval = 5 * time.Minute
|
|
|
|
// Snapshot ist das Ergebnis einer Erhebung.
|
|
type Snapshot struct {
|
|
CollectedAt time.Time
|
|
Duration time.Duration // wie lange die Erhebung brauchte
|
|
|
|
Workspaces []Workspace // nach Name sortiert
|
|
Total store.Inventory
|
|
DiskUsage store.DiskUsage // aller Workspaces zusammen
|
|
|
|
// SchemaVersion: die neueste Migration, die dieses Programm kennt. Ein
|
|
// Workspace, der davon abweicht, steht mit seinem Stand in Workspace.Schema.
|
|
SchemaVersion string
|
|
|
|
Days []Tag // genau Tage Einträge, der älteste zuerst, heute zuletzt
|
|
Projects []Project // mit Aktivität, die aktivsten zuerst
|
|
Idle []Project // ohne jede Aktivität im Zeitraum, nach Workspace und Name
|
|
|
|
// WaitCount zählt, wie oft Anfragen seit dem Start auf eine freie
|
|
// Datenbankverbindung warten mussten (alle Pools zusammen), zum Zeitpunkt
|
|
// der Erhebung. PrevWaitCount und PrevCollectedAt sind dieselbe Zahl bei
|
|
// der vorigen Erhebung (0 und Nullzeitpunkt bei der ersten). Die
|
|
// Differenz zeigt, ob gerade gewartet wird — die Summe seit dem Start
|
|
// sagt das nach Tagen Laufzeit nicht mehr.
|
|
WaitCount int64
|
|
PrevWaitCount int64
|
|
PrevCollectedAt time.Time
|
|
|
|
// Errors sind Workspaces, die sich nicht lesen ließen. Ihre Zahlen fehlen
|
|
// in den Summen; die übrigen sind trotzdem da.
|
|
Errors []string
|
|
}
|
|
|
|
// Workspace sind die Kennzahlen eines Workspace.
|
|
type Workspace struct {
|
|
ID, Name string
|
|
Location string // Datei oder Schema (store.Backend.Ort)
|
|
Inventory store.Inventory
|
|
DiskUsage store.DiskUsage
|
|
Schema string // zuletzt angewandte Migration
|
|
}
|
|
|
|
// SchemaMismatch sagt, ob der Workspace auf einem anderen Stand ist als das
|
|
// Programm.
|
|
func (w Workspace) SchemaMismatch(expected string) bool { return w.Schema != expected }
|
|
|
|
// Tag ist die Aktivität eines Tages über alle Workspaces.
|
|
type Tag struct {
|
|
Date time.Time // 0 Uhr UTC
|
|
AgentWrites int
|
|
HumanWrites int
|
|
Searches int
|
|
}
|
|
|
|
// Writes sind alle Schreibvorgänge des Tages.
|
|
func (t Tag) Writes() int { return t.AgentWrites + t.HumanWrites }
|
|
|
|
// Project ist die Aktivität eines Projekts im Zeitraum.
|
|
type Project struct {
|
|
Workspace string // Name
|
|
ID string
|
|
Slug string
|
|
Name string
|
|
|
|
AgentWrites int
|
|
HumanWrites int
|
|
Searches int
|
|
}
|
|
|
|
// Writes sind alle Schreibvorgänge des Projekts im Zeitraum.
|
|
func (p Project) Writes() int { return p.AgentWrites + p.HumanWrites }
|
|
|
|
// Total ist alles zusammen — danach wird sortiert.
|
|
func (p Project) Total() int { return p.Writes() + p.Searches }
|
|
|
|
// Collector erhebt den Stand und merkt ihn sich für MinInterval.
|
|
type Collector struct {
|
|
control *store.ControlStore
|
|
workspaces *workspace.Manager
|
|
|
|
// mu schützt stand und sorgt zugleich dafür, dass bei zwei
|
|
// gleichzeitigen Aufrufen nur einer rechnet; der zweite wartet und
|
|
// bekommt dasselbe Ergebnis.
|
|
mu sync.Mutex
|
|
snapshot Snapshot
|
|
}
|
|
|
|
// NewCollector legt einen Sammler an.
|
|
func NewCollector(control *store.ControlStore, workspaces *workspace.Manager) *Collector {
|
|
return &Collector{control: control, workspaces: workspaces}
|
|
}
|
|
|
|
// Current liefert einen Stand, der höchstens MinInterval alt ist, und
|
|
// erhebt dafür nötigenfalls neu.
|
|
func (s *Collector) Current(ctx context.Context, now time.Time) Snapshot {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if !s.snapshot.CollectedAt.IsZero() && now.Sub(s.snapshot.CollectedAt) < MinInterval {
|
|
return s.snapshot
|
|
}
|
|
newer := s.collect(ctx, now)
|
|
newer.PrevWaitCount, newer.PrevCollectedAt = s.snapshot.WaitCount, s.snapshot.CollectedAt
|
|
s.snapshot = newer
|
|
return s.snapshot
|
|
}
|
|
|
|
// WaitCount zählt die Wartevorgänge aller gerade offenen Pools zusammen: der
|
|
// Kontroll-Datenbank und jedes geöffneten Workspace.
|
|
func (s *Collector) WaitCount() int64 {
|
|
total := s.control.PoolStats().WaitCount()
|
|
for _, w := range s.workspaces.OpenWorkspaces() {
|
|
total += w.Store.PoolStats().WaitCount()
|
|
}
|
|
return total
|
|
}
|
|
|
|
// ForWorkspace erhebt den Stand eines einzelnen Workspace — für den
|
|
// Überblick des Workspace-Admins (AUSBAUSTUFEN.md 5.17). Ohne Zwischenspeicher:
|
|
// Ein Workspace sind nur eine Handvoll Abfragen, und der Admin soll nach
|
|
// einer Änderung (Key angelegt) sofort den neuen Stand sehen.
|
|
func (s *Collector) ForWorkspace(ctx context.Context, w store.Workspace, now time.Time) Snapshot {
|
|
return s.collectAll(ctx, now, []store.Workspace{w})
|
|
}
|
|
|
|
// collect fragt jeden Workspace ab und zählt zusammen.
|
|
//
|
|
// Dafür werden alle Workspaces geöffnet, auch lange nicht benutzte. Das tun
|
|
// das tägliche Aufräumen und der Export (main.go) ohnehin schon beim Start;
|
|
// zusätzliche Verbindungen kostet die Erhebung also nicht.
|
|
func (s *Collector) collect(ctx context.Context, now time.Time) Snapshot {
|
|
all, err := s.control.ListWorkspaces(ctx)
|
|
if err != nil {
|
|
return Snapshot{CollectedAt: now, Errors: []string{fmt.Sprintf("Workspaces konnten nicht gelesen werden: %v", err)}}
|
|
}
|
|
return s.collectAll(ctx, now, all)
|
|
}
|
|
|
|
// collectAll zählt die gegebenen Workspaces zusammen.
|
|
func (s *Collector) collectAll(ctx context.Context, now time.Time, all []store.Workspace) Snapshot {
|
|
begin := time.Now()
|
|
snapshot := Snapshot{CollectedAt: now}
|
|
|
|
// Die Tage stehen von vornherein fest, auch die ohne jede Aktivität —
|
|
// sonst hätte das Diagramm Lücken, die man nicht sieht.
|
|
today := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
|
first := today.AddDate(0, 0, -(Days - 1))
|
|
tagNum := map[string]int{}
|
|
for i := 0; i < Days; i++ {
|
|
d := first.AddDate(0, 0, i)
|
|
snapshot.Days = append(snapshot.Days, Tag{Date: d})
|
|
tagNum[d.Format("2006-01-02")] = i
|
|
}
|
|
|
|
for _, w := range all {
|
|
ws, projects, err := s.collectWorkspace(ctx, w, now, first, &snapshot, tagNum)
|
|
if err != nil {
|
|
snapshot.Errors = append(snapshot.Errors, fmt.Sprintf("%s: %v", w.Name, err))
|
|
continue
|
|
}
|
|
snapshot.Workspaces = append(snapshot.Workspaces, ws)
|
|
snapshot.Total = snapshot.Total.Plus(ws.Inventory)
|
|
snapshot.DiskUsage.Data += ws.DiskUsage.Data
|
|
snapshot.DiskUsage.WAL += ws.DiskUsage.WAL
|
|
|
|
for _, p := range projects {
|
|
if p.Total() > 0 {
|
|
snapshot.Projects = append(snapshot.Projects, p)
|
|
} else {
|
|
snapshot.Idle = append(snapshot.Idle, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(snapshot.Workspaces, func(i, j int) bool { return snapshot.Workspaces[i].Name < snapshot.Workspaces[j].Name })
|
|
// Die aktivsten zuerst; bei Gleichstand alphabetisch, damit die
|
|
// Reihenfolge beim Neuladen nicht springt.
|
|
sort.Slice(snapshot.Projects, func(i, j int) bool {
|
|
a, b := snapshot.Projects[i], snapshot.Projects[j]
|
|
if a.Total() != b.Total() {
|
|
return a.Total() > b.Total()
|
|
}
|
|
return a.Workspace+a.Name < b.Workspace+b.Name
|
|
})
|
|
sort.Slice(snapshot.Idle, func(i, j int) bool {
|
|
a, b := snapshot.Idle[i], snapshot.Idle[j]
|
|
return a.Workspace+a.Name < b.Workspace+b.Name
|
|
})
|
|
|
|
snapshot.WaitCount = s.WaitCount()
|
|
snapshot.Duration = time.Since(begin)
|
|
return snapshot
|
|
}
|
|
|
|
// collectWorkspace sammelt die Zahlen eines Workspace. Die Aktivität je Tag
|
|
// zählt sie direkt in snapshot.Days ein; die je Projekt gibt sie zurück.
|
|
func (s *Collector) collectWorkspace(ctx context.Context, w store.Workspace, now, since time.Time,
|
|
snapshot *Snapshot, tagNum map[string]int) (Workspace, []Project, error) {
|
|
|
|
result := Workspace{ID: w.ID, Name: w.Name, Location: s.workspaces.Location(w.ID)}
|
|
|
|
opened, err := s.workspaces.Get(w.ID)
|
|
if err != nil {
|
|
return result, nil, err
|
|
}
|
|
st := opened.Store
|
|
|
|
if result.Inventory, err = st.Inventory(ctx, now); err != nil {
|
|
return result, nil, err
|
|
}
|
|
if result.DiskUsage, err = st.DiskUsage(ctx); err != nil {
|
|
return result, nil, err
|
|
}
|
|
actual, expected, err := st.SchemaVersion(ctx)
|
|
if err != nil {
|
|
return result, nil, err
|
|
}
|
|
result.Schema = actual
|
|
snapshot.SchemaVersion = expected // für alle Workspaces derselbe: er kommt aus dem Programm
|
|
|
|
// Jedes Projekt bekommt einen Eintrag, auch ohne Aktivität — gerade die
|
|
// ruhigen sind interessant.
|
|
list, err := st.ListProjects(ctx, w.ID)
|
|
if err != nil {
|
|
return result, nil, err
|
|
}
|
|
projects := make([]Project, len(list))
|
|
toID := map[string]*Project{}
|
|
for i, p := range list {
|
|
projects[i] = Project{Workspace: w.Name, ID: p.ID, Slug: p.Slug, Name: p.Name}
|
|
toID[p.ID] = &projects[i]
|
|
}
|
|
|
|
activity, err := st.Activity(ctx, since)
|
|
if err != nil {
|
|
return result, nil, err
|
|
}
|
|
for _, a := range activity {
|
|
if i, ok := tagNum[a.Tag]; ok {
|
|
snapshot.Days[i].AgentWrites += a.AgentWrites
|
|
snapshot.Days[i].HumanWrites += a.HumanWrites
|
|
snapshot.Days[i].Searches += a.Searches
|
|
}
|
|
// Zeilen eines inzwischen gelöschten Projekts zählen für den Tag,
|
|
// haben aber kein Projekt mehr, dem sie gehören.
|
|
if p := toID[a.ProjectID]; p != nil {
|
|
p.AgentWrites += a.AgentWrites
|
|
p.HumanWrites += a.HumanWrites
|
|
p.Searches += a.Searches
|
|
}
|
|
}
|
|
|
|
return result, projects, nil
|
|
}
|