feat: Interface-Durchsatz-Chart — Backend berechnet RX/TX-Rate (bps), Frontend zeigt Dual-Sparkline per Klick auf Interface

This commit is contained in:
cynfo3000
2026-03-06 22:00:33 +01:00
parent eb5266f848
commit 9cb504fea1
3 changed files with 137 additions and 43 deletions
+39 -8
View File
@@ -15,8 +15,15 @@ import (
_ "github.com/jackc/pgx/v5/stdlib"
)
type ifaceSnapshot struct {
rxBytes float64
txBytes float64
ts time.Time
}
type Database struct {
db *sql.DB
db *sql.DB
lastIfaceSnap map[string]map[string]ifaceSnapshot // agentID -> ifaceName -> snapshot
}
type DBConfig struct {
@@ -49,7 +56,7 @@ func New(cfg DBConfig) (*Database, error) {
return nil, fmt.Errorf("Datenbank-Verbindung fehlgeschlagen: %w", err)
}
d := &Database{db: db}
d := &Database{db: db, lastIfaceSnap: make(map[string]map[string]ifaceSnapshot)}
if err := d.migrate(); err != nil {
return nil, fmt.Errorf("Migration fehlgeschlagen: %w", err)
}
@@ -415,13 +422,32 @@ func (d *Database) writeMetrics(tx *sql.Tx, agentID string, ts time.Time, data *
}
}
// Network Interfaces
// Network Interfaces — kumulierte Bytes + abgeleitete Rate (bytes/s)
if _, ok := d.lastIfaceSnap[agentID]; !ok {
d.lastIfaceSnap[agentID] = make(map[string]ifaceSnapshot)
}
for _, iface := range data.NetworkInterfaces {
if iface.RxBytes > 0 || iface.TxBytes > 0 {
tags, _ := json.Marshal(map[string]string{"interface": iface.Name})
stmt.Exec(ts, agentID, "interface_rx_bytes", float64(iface.RxBytes), string(tags))
stmt.Exec(ts, agentID, "interface_tx_bytes", float64(iface.TxBytes), string(tags))
rx := float64(iface.RxBytes)
tx := float64(iface.TxBytes)
tags, _ := json.Marshal(map[string]string{"interface": iface.Name})
tagsStr := string(tags)
if rx > 0 || tx > 0 {
stmt.Exec(ts, agentID, "interface_rx_bytes", rx, tagsStr)
stmt.Exec(ts, agentID, "interface_tx_bytes", tx, tagsStr)
}
// Rate berechnen wenn vorheriger Snapshot vorhanden
if prev, ok := d.lastIfaceSnap[agentID][iface.Name]; ok {
elapsed := ts.Sub(prev.ts).Seconds()
if elapsed > 0 && elapsed < 300 { // max 5 min Lücke ignorieren
rxRate := (rx - prev.rxBytes) / elapsed
txRate := (tx - prev.txBytes) / elapsed
if rxRate >= 0 && txRate >= 0 { // negative Werte ignorieren (Reboot)
stmt.Exec(ts, agentID, "interface_rx_bps", rxRate, tagsStr)
stmt.Exec(ts, agentID, "interface_tx_bps", txRate, tagsStr)
}
}
}
d.lastIfaceSnap[agentID][iface.Name] = ifaceSnapshot{rxBytes: rx, txBytes: tx, ts: ts}
}
// PF States
@@ -858,7 +884,7 @@ func (d *Database) DeleteConfigBackup(agentID string, backupID int64) (bool, err
// --- Metrics Abfragen ---
// GetMetrics holt Metriken fuer einen Agent in einem Zeitraum
func (d *Database) GetMetrics(agentID, metric string, from, to time.Time, limit int) ([]map[string]interface{}, error) {
func (d *Database) GetMetrics(agentID, metric, tagsFilter string, from, to time.Time, limit int) ([]map[string]interface{}, error) {
if limit <= 0 {
limit = 1000
}
@@ -872,6 +898,11 @@ func (d *Database) GetMetrics(agentID, metric string, from, to time.Time, limit
args = append(args, metric)
}
if tagsFilter != "" {
query += fmt.Sprintf(" AND tags @> $%d::jsonb", len(args)+1)
args = append(args, tagsFilter)
}
query += " ORDER BY time DESC LIMIT $" + fmt.Sprintf("%d", len(args)+1)
args = append(args, limit)