Accountant: Inventar, where, Checkmk, Abrechnung
This commit is contained in:
1 parent
9f16148cde
commit
dd93277750
59 files changed
+7522
-113
No files matched your search
@@ -0,0 +1,236 @@
|
||||
package accountant
|
||||
|
||||
// Abfragen auf dem Accountant (Spec 5.3, Roadmap 6d Schritt 1): where – wo
|
||||
// läuft eine URL, seit wann, wo vorher? – und routes – alle aktuellen
|
||||
// Routen. Aktuell = letzter Bericht je Payee; seit wann/vorher = Historie.
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/ledger"
|
||||
)
|
||||
|
||||
// RouteRecords übersetzt die Routen eines Berichts für die Historie.
|
||||
func RouteRecords(rep Report) []ledger.RouteRecord {
|
||||
out := make([]ledger.RouteRecord, 0, len(rep.Routes))
|
||||
for _, r := range rep.Routes {
|
||||
out = append(out, ledger.RouteRecord{
|
||||
Tenant: r.Tenant, Node: r.Node, Protocol: r.Protocol, Router: r.Router, Hosts: r.Hosts,
|
||||
Backend: r.Backend, BackendFQDN: r.BackendFQDN, Workload: r.Workload, ComposeDir: r.ComposeDir, Product: r.Product,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CurrentRoute ist eine Route aus dem letzten Bericht eines Payees.
|
||||
type CurrentRoute struct {
|
||||
Payee string
|
||||
ReportAt time.Time // Eingang des Berichts
|
||||
Route ReportRoute
|
||||
Node ReportNode // Knoten der Route (leer, falls nicht im Bericht)
|
||||
TenantName string
|
||||
// Customer: zugeordneter Kunde (nil = keinem Kunden zugeordnet).
|
||||
Customer *ledger.Customer
|
||||
Since time.Time // Beginn des Zeitraums an diesem Ort (Historie)
|
||||
// Previous: frühere, beendete Orte derselben Route (Mandant, Protokoll,
|
||||
// Router), neueste zuerst.
|
||||
Previous []ledger.RouteSpan
|
||||
}
|
||||
|
||||
// WhereResult ist das Ergebnis von Where.
|
||||
type WhereResult struct {
|
||||
Current []CurrentRoute
|
||||
// Gone: passende Routen, die es nirgends mehr gibt (neueste zuerst).
|
||||
Gone []ledger.RouteSpan
|
||||
}
|
||||
|
||||
// RoutesFilter schränkt Routes ein; leere Felder filtern nicht.
|
||||
type RoutesFilter struct {
|
||||
Tenant string
|
||||
Payee string
|
||||
}
|
||||
|
||||
func spanKey(tenant, node, protocol, router string) string {
|
||||
return tenant + "\x00" + node + "\x00" + protocol + "\x00" + router
|
||||
}
|
||||
|
||||
func routeKey(tenant, protocol, router string) string {
|
||||
return tenant + "\x00" + protocol + "\x00" + router
|
||||
}
|
||||
|
||||
// currentRoutes liest die letzten Berichte aller Payees und ergänzt den
|
||||
// Beginn je Ort aus der Historie.
|
||||
func currentRoutes(ctx context.Context, l *ledger.Ledger, keep func(ReportRoute) bool) ([]CurrentRoute, error) {
|
||||
payees, err := l.ListPayees(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
open, err := l.RouteHistory(ctx, ledger.HistoryFilter{OpenOnly: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolver, err := l.CustomerResolver(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
customers := map[string]*ledger.Customer{}
|
||||
cs, err := l.ListCustomers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range cs {
|
||||
customers[cs[i].ID] = &cs[i]
|
||||
}
|
||||
since := map[string]time.Time{}
|
||||
for _, s := range open {
|
||||
since[s.Payee+"\x00"+spanKey(s.Tenant, s.Node, s.Protocol, s.Router)] = s.FirstSeen
|
||||
}
|
||||
var out []CurrentRoute
|
||||
for _, p := range payees {
|
||||
body, at, err := l.LatestReport(ctx, p.ID)
|
||||
if err != nil {
|
||||
continue // noch kein Bericht
|
||||
}
|
||||
var rep Report
|
||||
if err := json.Unmarshal(body, &rep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes := map[string]ReportNode{}
|
||||
for _, n := range rep.Nodes {
|
||||
nodes[n.ID] = n
|
||||
}
|
||||
names := map[string]string{}
|
||||
for _, t := range rep.Tenants {
|
||||
names[t.ID] = t.Name
|
||||
}
|
||||
for _, r := range rep.Routes {
|
||||
if !keep(r) {
|
||||
continue
|
||||
}
|
||||
out = append(out, CurrentRoute{
|
||||
Payee: p.ID, ReportAt: at, Route: r, Node: nodes[r.Node], TenantName: names[r.Tenant],
|
||||
Customer: customers[resolver.Resolve(p.ID, r.Tenant)],
|
||||
Since: since[p.ID+"\x00"+spanKey(r.Tenant, r.Node, r.Protocol, r.Router)],
|
||||
})
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b CurrentRoute) int {
|
||||
return cmp.Or(strings.Compare(firstHost(a.Route.Hosts), firstHost(b.Route.Hosts)),
|
||||
strings.Compare(a.Payee, b.Payee), strings.Compare(a.Route.Node, b.Route.Node), strings.Compare(a.Route.Router, b.Route.Router))
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func firstHost(hosts []string) string {
|
||||
if len(hosts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return hosts[0]
|
||||
}
|
||||
|
||||
// matches: ein Host enthält query, oder query ist die Mandanten-ID.
|
||||
func matches(query, tenant string, hosts []string) bool {
|
||||
if strings.EqualFold(tenant, query) {
|
||||
return true
|
||||
}
|
||||
for _, h := range hosts {
|
||||
if strings.Contains(strings.ToLower(h), query) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Where sucht Routen, deren Host query enthält (ohne Groß-/Kleinschreibung)
|
||||
// oder deren Mandant query heißt – aktuell mit früheren Orten, dazu
|
||||
// passende Routen, die es nicht mehr gibt.
|
||||
func Where(ctx context.Context, l *ledger.Ledger, query string) (WhereResult, error) {
|
||||
query = strings.ToLower(strings.TrimSpace(query))
|
||||
var res WhereResult
|
||||
if query == "" {
|
||||
return res, nil
|
||||
}
|
||||
cur, err := currentRoutes(ctx, l, func(r ReportRoute) bool { return matches(query, r.Tenant, r.Hosts) })
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
all, err := l.RouteHistory(ctx, ledger.HistoryFilter{})
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
ended := map[string][]ledger.RouteSpan{}
|
||||
for _, s := range all {
|
||||
if !s.Ended.IsZero() {
|
||||
k := routeKey(s.Tenant, s.Protocol, s.Router)
|
||||
ended[k] = append(ended[k], s)
|
||||
}
|
||||
}
|
||||
newestFirst := func(a, b ledger.RouteSpan) int { return b.Ended.Compare(a.Ended) }
|
||||
alive := map[string]bool{}
|
||||
for i := range cur {
|
||||
r := cur[i].Route
|
||||
k := routeKey(r.Tenant, r.Protocol, r.Router)
|
||||
alive[k] = true
|
||||
for _, s := range ended[k] {
|
||||
if s.Payee == cur[i].Payee && s.Node == r.Node {
|
||||
continue // derselbe Ort früher einmal – kein Umzug
|
||||
}
|
||||
cur[i].Previous = append(cur[i].Previous, s)
|
||||
}
|
||||
slices.SortFunc(cur[i].Previous, newestFirst)
|
||||
}
|
||||
res.Current = cur
|
||||
for k, spans := range ended {
|
||||
if alive[k] {
|
||||
continue
|
||||
}
|
||||
for _, s := range spans {
|
||||
if matches(query, s.Tenant, s.Hosts) {
|
||||
res.Gone = append(res.Gone, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.SortFunc(res.Gone, newestFirst)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Routes liefert alle aktuellen Routen, sortiert nach Host.
|
||||
func Routes(ctx context.Context, l *ledger.Ledger, f RoutesFilter) ([]CurrentRoute, error) {
|
||||
cur, err := currentRoutes(ctx, l, func(r ReportRoute) bool { return f.Tenant == "" || r.Tenant == f.Tenant })
|
||||
if err != nil || f.Payee == "" {
|
||||
return cur, err
|
||||
}
|
||||
return slices.DeleteFunc(cur, func(c CurrentRoute) bool { return c.Payee != f.Payee }), nil
|
||||
}
|
||||
|
||||
// ItemKey bezeichnet einen Posten: Mandant, Protokoll, Router (über alle
|
||||
// Payees und Knoten – ein Umzug bleibt derselbe Posten).
|
||||
type ItemKey struct {
|
||||
Tenant, Protocol, Router string
|
||||
}
|
||||
|
||||
// ItemsForHost liefert die Posten, zu denen host gehört (exakter Host, aus
|
||||
// der Historie; ohne Groß-/Kleinschreibung), sortiert.
|
||||
func ItemsForHost(ctx context.Context, l *ledger.Ledger, host string) ([]ItemKey, error) {
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
spans, err := l.RouteHistory(ctx, ledger.HistoryFilter{Host: host})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []ItemKey
|
||||
for _, s := range spans {
|
||||
k := ItemKey{s.Tenant, s.Protocol, s.Router}
|
||||
if slices.Contains(s.Hosts, host) && !slices.Contains(out, k) {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b ItemKey) int {
|
||||
return cmp.Or(strings.Compare(a.Tenant, b.Tenant), strings.Compare(a.Router, b.Router), strings.Compare(a.Protocol, b.Protocol))
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user