diff --git a/backend/api/metrics.go b/backend/api/metrics.go index 4536497..be7cddd 100644 --- a/backend/api/metrics.go +++ b/backend/api/metrics.go @@ -34,7 +34,8 @@ func (h *Handler) getMetrics() http.HandlerFunc { } } - results, err := h.db.GetMetrics(agentID, metric, from, to, limit) + tagsFilter := r.URL.Query().Get("tags") + results, err := h.db.GetMetrics(agentID, metric, tagsFilter, from, to, limit) if err != nil { writeError(w, http.StatusInternalServerError, "Metriken laden fehlgeschlagen") return diff --git a/backend/db/postgres.go b/backend/db/postgres.go index 90be336..741eaff 100644 --- a/backend/db/postgres.go +++ b/backend/db/postgres.go @@ -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) diff --git a/frontend/src/components/AgentPanel.jsx b/frontend/src/components/AgentPanel.jsx index 492c50b..b9e000b 100644 --- a/frontend/src/components/AgentPanel.jsx +++ b/frontend/src/components/AgentPanel.jsx @@ -108,7 +108,7 @@ export default function AgentPanel({ agentId, customers, onClose, onReload }) { if (!sys) return