munin/internal/statistik/statistik.go

290 lines
9.8 KiB
Go

// Package statistik 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 MindestAbstand 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 statistik
import (
"context"
"fmt"
"sort"
"sync"
"time"
"munin/internal/store"
"munin/internal/workspace"
)
// Tage 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 Tage = store.UsageWindowDays
// MindestAbstand ist die Zeit, die ein Ergebnis gilt, bevor neu gerechnet
// wird.
const MindestAbstand = 5 * time.Minute
// Stand ist das Ergebnis einer Erhebung.
type Stand struct {
Erhoben time.Time
Dauer time.Duration // wie lange die Erhebung brauchte
Workspaces []Workspace // nach Name sortiert
Summe store.Bestand
Belegung store.Belegung // aller Workspaces zusammen
// Schemastand: die neueste Migration, die dieses Programm kennt. Ein
// Workspace, der davon abweicht, steht mit seinem Stand in Workspace.Schema.
Schemastand string
Tage []Tag // genau Tage Einträge, der älteste zuerst, heute zuletzt
Projekte []Projekt // mit Aktivität, die aktivsten zuerst
Ruhig []Projekt // ohne jede Aktivität im Zeitraum, nach Workspace und Name
// Gewartet zählt, wie oft Anfragen seit dem Start auf eine freie
// Datenbankverbindung warten mussten (alle Pools zusammen), zum Zeitpunkt
// der Erhebung. GewartetVorher und VorherErhoben 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.
Gewartet int64
GewartetVorher int64
VorherErhoben time.Time
// Fehler sind Workspaces, die sich nicht lesen ließen. Ihre Zahlen fehlen
// in den Summen; die übrigen sind trotzdem da.
Fehler []string
}
// Workspace sind die Kennzahlen eines Workspace.
type Workspace struct {
ID, Name string
Ort string // Datei oder Schema (store.Speicher.Ort)
Bestand store.Bestand
Belegung store.Belegung
Schema string // zuletzt angewandte Migration
}
// SchemaAbweichend sagt, ob der Workspace auf einem anderen Stand ist als das
// Programm.
func (w Workspace) SchemaAbweichend(soll string) bool { return w.Schema != soll }
// Tag ist die Aktivität eines Tages über alle Workspaces.
type Tag struct {
Datum time.Time // 0 Uhr UTC
SchreibenAgent int
SchreibenMensch int
Suchen int
}
// Schreiben sind alle Schreibvorgänge des Tages.
func (t Tag) Schreiben() int { return t.SchreibenAgent + t.SchreibenMensch }
// Projekt ist die Aktivität eines Projekts im Zeitraum.
type Projekt struct {
Workspace string // Name
ID string
Slug string
Name string
SchreibenAgent int
SchreibenMensch int
Suchen int
}
// Schreiben sind alle Schreibvorgänge des Projekts im Zeitraum.
func (p Projekt) Schreiben() int { return p.SchreibenAgent + p.SchreibenMensch }
// Summe ist alles zusammen — danach wird sortiert.
func (p Projekt) Summe() int { return p.Schreiben() + p.Suchen }
// Sammler erhebt den Stand und merkt ihn sich für MindestAbstand.
type Sammler 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
stand Stand
}
// NewSammler legt einen Sammler an.
func NewSammler(control *store.ControlStore, workspaces *workspace.Manager) *Sammler {
return &Sammler{control: control, workspaces: workspaces}
}
// Aktuell liefert einen Stand, der höchstens MindestAbstand alt ist, und
// erhebt dafür nötigenfalls neu.
func (s *Sammler) Aktuell(ctx context.Context, jetzt time.Time) Stand {
s.mu.Lock()
defer s.mu.Unlock()
if !s.stand.Erhoben.IsZero() && jetzt.Sub(s.stand.Erhoben) < MindestAbstand {
return s.stand
}
neu := s.erhebe(ctx, jetzt)
neu.GewartetVorher, neu.VorherErhoben = s.stand.Gewartet, s.stand.Erhoben
s.stand = neu
return s.stand
}
// Gewartet zählt die Wartevorgänge aller gerade offenen Pools zusammen: der
// Kontroll-Datenbank und jedes geöffneten Workspace.
func (s *Sammler) Gewartet() int64 {
summe := s.control.Verbindungen().Gewartet()
for _, w := range s.workspaces.Offen() {
summe += w.Store.Verbindungen().Gewartet()
}
return summe
}
// FuerWorkspace 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 *Sammler) FuerWorkspace(ctx context.Context, w store.Workspace, jetzt time.Time) Stand {
return s.erhebeListe(ctx, jetzt, []store.Workspace{w})
}
// erhebe 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 *Sammler) erhebe(ctx context.Context, jetzt time.Time) Stand {
alle, err := s.control.ListWorkspaces(ctx)
if err != nil {
return Stand{Erhoben: jetzt, Fehler: []string{fmt.Sprintf("Workspaces konnten nicht gelesen werden: %v", err)}}
}
return s.erhebeListe(ctx, jetzt, alle)
}
// erhebeListe zählt die gegebenen Workspaces zusammen.
func (s *Sammler) erhebeListe(ctx context.Context, jetzt time.Time, alle []store.Workspace) Stand {
beginn := time.Now()
stand := Stand{Erhoben: jetzt}
// Die Tage stehen von vornherein fest, auch die ohne jede Aktivität —
// sonst hätte das Diagramm Lücken, die man nicht sieht.
heute := time.Date(jetzt.UTC().Year(), jetzt.UTC().Month(), jetzt.UTC().Day(), 0, 0, 0, 0, time.UTC)
erster := heute.AddDate(0, 0, -(Tage - 1))
tagNr := map[string]int{}
for i := 0; i < Tage; i++ {
d := erster.AddDate(0, 0, i)
stand.Tage = append(stand.Tage, Tag{Datum: d})
tagNr[d.Format("2006-01-02")] = i
}
for _, w := range alle {
ws, projekte, err := s.erheheWorkspace(ctx, w, jetzt, erster, &stand, tagNr)
if err != nil {
stand.Fehler = append(stand.Fehler, fmt.Sprintf("%s: %v", w.Name, err))
continue
}
stand.Workspaces = append(stand.Workspaces, ws)
stand.Summe = stand.Summe.Plus(ws.Bestand)
stand.Belegung.Daten += ws.Belegung.Daten
stand.Belegung.WAL += ws.Belegung.WAL
for _, p := range projekte {
if p.Summe() > 0 {
stand.Projekte = append(stand.Projekte, p)
} else {
stand.Ruhig = append(stand.Ruhig, p)
}
}
}
sort.Slice(stand.Workspaces, func(i, j int) bool { return stand.Workspaces[i].Name < stand.Workspaces[j].Name })
// Die aktivsten zuerst; bei Gleichstand alphabetisch, damit die
// Reihenfolge beim Neuladen nicht springt.
sort.Slice(stand.Projekte, func(i, j int) bool {
a, b := stand.Projekte[i], stand.Projekte[j]
if a.Summe() != b.Summe() {
return a.Summe() > b.Summe()
}
return a.Workspace+a.Name < b.Workspace+b.Name
})
sort.Slice(stand.Ruhig, func(i, j int) bool {
a, b := stand.Ruhig[i], stand.Ruhig[j]
return a.Workspace+a.Name < b.Workspace+b.Name
})
stand.Gewartet = s.Gewartet()
stand.Dauer = time.Since(beginn)
return stand
}
// erheheWorkspace sammelt die Zahlen eines Workspace. Die Aktivität je Tag
// zählt sie direkt in stand.Tage ein; die je Projekt gibt sie zurück.
func (s *Sammler) erheheWorkspace(ctx context.Context, w store.Workspace, jetzt, seit time.Time,
stand *Stand, tagNr map[string]int) (Workspace, []Projekt, error) {
ergebnis := Workspace{ID: w.ID, Name: w.Name, Ort: s.workspaces.Ort(w.ID)}
offen, err := s.workspaces.Get(w.ID)
if err != nil {
return ergebnis, nil, err
}
st := offen.Store
if ergebnis.Bestand, err = st.Bestand(ctx, jetzt); err != nil {
return ergebnis, nil, err
}
if ergebnis.Belegung, err = st.Belegung(ctx); err != nil {
return ergebnis, nil, err
}
ist, soll, err := st.Schemastand(ctx)
if err != nil {
return ergebnis, nil, err
}
ergebnis.Schema = ist
stand.Schemastand = soll // für alle Workspaces derselbe: er kommt aus dem Programm
// Jedes Projekt bekommt einen Eintrag, auch ohne Aktivität — gerade die
// ruhigen sind interessant.
liste, err := st.ListProjects(ctx, w.ID)
if err != nil {
return ergebnis, nil, err
}
projekte := make([]Projekt, len(liste))
nachID := map[string]*Projekt{}
for i, p := range liste {
projekte[i] = Projekt{Workspace: w.Name, ID: p.ID, Slug: p.Slug, Name: p.Name}
nachID[p.ID] = &projekte[i]
}
aktivitaet, err := st.Aktivitaet(ctx, seit)
if err != nil {
return ergebnis, nil, err
}
for _, a := range aktivitaet {
if i, ok := tagNr[a.Tag]; ok {
stand.Tage[i].SchreibenAgent += a.SchreibenAgent
stand.Tage[i].SchreibenMensch += a.SchreibenMensch
stand.Tage[i].Suchen += a.Suchen
}
// Zeilen eines inzwischen gelöschten Projekts zählen für den Tag,
// haben aber kein Projekt mehr, dem sie gehören.
if p := nachID[a.ProjektID]; p != nil {
p.SchreibenAgent += a.SchreibenAgent
p.SchreibenMensch += a.SchreibenMensch
p.Suchen += a.Suchen
}
}
return ergebnis, projekte, nil
}