// 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 }