// Copyright 2025, IPng Networks GmbH, Pim van Pelt package vpp import ( "time" "go.fd.io/govpp/api" "govpp-snmp-agentx/logger" ) // StatsCallback is called when interface stats are retrieved type StatsCallback func(*api.InterfaceStats) // StatsManager handles VPP statistics operations type StatsManager struct { client *VPPClient interfaceManager *InterfaceManager statsCallback StatsCallback period time.Duration running bool } // NewStatsManager creates a new stats manager func NewStatsManager(client *VPPClient, interfaceManager *InterfaceManager) *StatsManager { return &StatsManager{ client: client, interfaceManager: interfaceManager, period: time.Duration(*Period) * time.Second, } } // SetStatsCallback sets the callback for stats updates func (sm *StatsManager) SetStatsCallback(callback StatsCallback) { sm.statsCallback = callback } // SetPeriod sets the polling period for stats func (sm *StatsManager) SetPeriod(period time.Duration) { sm.period = period } // StartStatsRoutine starts the stats polling routine func (sm *StatsManager) StartStatsRoutine() { if sm.running { logger.Debugf("Stats routine already running") return } sm.running = true go sm.statsRoutine() } // StopStatsRoutine stops the stats polling routine func (sm *StatsManager) StopStatsRoutine() { sm.running = false } // GetInterfaceStats retrieves current interface statistics func (sm *StatsManager) GetInterfaceStats() (*api.InterfaceStats, error) { if !sm.client.IsConnected() { return nil, &VPPError{Message: "VPP client not connected"} } statsConn := sm.client.GetStatsConnection() if statsConn == nil { return nil, &VPPError{Message: "Stats connection not available"} } stats := new(api.InterfaceStats) if err := statsConn.GetInterfaceStats(stats); err != nil { return nil, err } return stats, nil } // statsRoutine is the main stats polling loop func (sm *StatsManager) statsRoutine() { logger.Debugf("Starting VPP stats routine with period: %v", sm.period) ticker := time.NewTicker(sm.period) defer ticker.Stop() var wasConnected = false for { if !sm.running { logger.Debugf("Stats routine stopping") break } // Check if we need to connect/reconnect if !sm.client.IsConnected() { if wasConnected { logger.Printf("VPP connection lost, attempting reconnect...") wasConnected = false } else { logger.Printf("VPP not connected, attempting connection...") } if err := sm.client.Connect(); err != nil { logger.Printf("Failed to connect to VPP: %v", err) time.Sleep(time.Second) continue } logger.Printf("VPP connection established") wasConnected = true } // Query stats if connected if sm.client.IsConnected() { if !sm.queryAndReportStats() { logger.Printf("Stats query failed, marking connection as lost") sm.client.Disconnect() continue } } // Wait for next tick <-ticker.C } logger.Debugf("Stats routine ended") } // queryAndReportStats queries stats and calls the callback func (sm *StatsManager) queryAndReportStats() bool { // Check VPP liveness first if !sm.client.CheckLiveness() { logger.Debugf("VPP liveness check failed") return false } // Get interface stats stats, err := sm.GetInterfaceStats() if err != nil { logger.Printf("Failed to get interface stats: %v", err) return false } // Filter out deleted interfaces by comparing with current interface list filteredStats, err := sm.filterValidInterfaces(stats) if err != nil { logger.Printf("Failed to filter interface stats: %v", err) return false } // Debug log basic info originalCount := len(stats.Interfaces) filteredCount := len(filteredStats.Interfaces) logger.Debugf("Retrieved stats for %d interfaces, filtered to %d valid interfaces", originalCount, filteredCount) if originalCount > filteredCount { logger.Debugf("Filtered out %d deleted interfaces", originalCount-filteredCount) } // Debug logging for individual interfaces for _, iface := range filteredStats.Interfaces { logger.Debugf("Interface %d (%s): RX %d pkts/%d bytes, TX %d pkts/%d bytes", iface.InterfaceIndex, iface.InterfaceName, iface.Rx.Packets, iface.Rx.Bytes, iface.Tx.Packets, iface.Tx.Bytes) } // Call the callback to update the MIB if sm.statsCallback != nil { sm.statsCallback(filteredStats) } return true } // filterValidInterfaces removes stats for deleted interfaces by comparing with current interface list func (sm *StatsManager) filterValidInterfaces(stats *api.InterfaceStats) (*api.InterfaceStats, error) { if sm.interfaceManager == nil { logger.Debugf("No interface manager available, returning unfiltered stats") return stats, nil } // Get current interface details currentInterfaces, err := sm.interfaceManager.GetAllInterfaceDetails() if err != nil { logger.Debugf("Failed to get current interface list: %v", err) return stats, err } // Create a map of valid sw_if_index values validInterfaces := make(map[uint32]bool) for _, iface := range currentInterfaces { validInterfaces[uint32(iface.SwIfIndex)] = true } // Filter the stats to only include valid interfaces filteredStats := &api.InterfaceStats{ Interfaces: make([]api.InterfaceCounters, 0, len(stats.Interfaces)), } for _, ifaceStat := range stats.Interfaces { if validInterfaces[ifaceStat.InterfaceIndex] { filteredStats.Interfaces = append(filteredStats.Interfaces, ifaceStat) } else { logger.Debugf("Filtering out stats for deleted interface %d (%s)", ifaceStat.InterfaceIndex, ifaceStat.InterfaceName) } } return filteredStats, nil }