290 lines
9.8 KiB
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
|
|
}
|