Browse Source

refactor(analytic): improved network stat #913

Jacky 3 months ago
parent
commit
789698a0d4

+ 4 - 12
api/analytic/analytic.go

@@ -11,7 +11,6 @@ import (
 	"github.com/shirou/gopsutil/v4/cpu"
 	"github.com/shirou/gopsutil/v4/host"
 	"github.com/shirou/gopsutil/v4/load"
-	"github.com/shirou/gopsutil/v4/net"
 	"github.com/spf13/cast"
 	"github.com/uozi-tech/cosy/logger"
 
@@ -75,15 +74,13 @@ func Analytic(c *gin.Context) {
 			continue
 		}
 
-		network, err := net.IOCounters(false)
+		network, err := analytic.GetNetworkStat()
 		if err != nil {
 			logger.Error(err)
 			continue
 		}
 
-		if len(network) > 0 {
-			stat.Network = network[0]
-		}
+		stat.Network = *network
 
 		// write
 		err = ws.WriteJSON(stat)
@@ -104,7 +101,7 @@ func GetAnalyticInit(c *gin.Context) {
 		logger.Error(err)
 	}
 
-	network, err := net.IOCounters(false)
+	network, err := analytic.GetNetworkStat()
 	if err != nil {
 		logger.Error(err)
 	}
@@ -119,11 +116,6 @@ func GetAnalyticInit(c *gin.Context) {
 		logger.Error(err)
 	}
 
-	var _net net.IOCountersStat
-	if len(network) > 0 {
-		_net = network[0]
-	}
-
 	hostInfo, err := host.Info()
 	if err != nil {
 		logger.Error(err)
@@ -151,7 +143,7 @@ func GetAnalyticInit(c *gin.Context) {
 			Total: analytic.CpuTotalRecord,
 		},
 		Network: NetworkRecords{
-			Init:      _net,
+			Init:      *network,
 			BytesRecv: analytic.NetRecvRecord,
 			BytesSent: analytic.NetSentRecord,
 		},

+ 5 - 7
internal/analytic/analytic.go

@@ -1,9 +1,9 @@
 package analytic
 
 import (
-	"github.com/shirou/gopsutil/v4/net"
-	"github.com/uozi-tech/cosy/logger"
 	"time"
+
+	"github.com/uozi-tech/cosy/logger"
 )
 
 type Usage[T uint64 | float64] struct {
@@ -25,14 +25,12 @@ var (
 )
 
 func init() {
-	network, err := net.IOCounters(false)
+	network, err := GetNetworkStat()
 	if err != nil {
 		logger.Error(err)
 	}
-	if len(network) > 0 {
-		LastNetRecv = network[0].BytesRecv
-		LastNetSent = network[0].BytesSent
-	}
+	LastNetRecv = network.BytesRecv
+	LastNetSent = network.BytesSent
 
 	LastDiskReads, LastDiskWrites = getTotalDiskIO()
 

+ 220 - 0
internal/analytic/network.go

@@ -0,0 +1,220 @@
+package analytic
+
+import (
+	stdnet "net"
+
+	"github.com/shirou/gopsutil/v4/net"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+func GetNetworkStat() (data *net.IOCountersStat, err error) {
+	networkStats, err := net.IOCounters(true)
+	if err != nil {
+		return
+	}
+	if len(networkStats) == 0 {
+		return &net.IOCountersStat{}, nil
+	}
+	// Get all network interfaces
+	interfaces, err := stdnet.Interfaces()
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	var (
+		totalBytesRecv   uint64
+		totalBytesSent   uint64
+		totalPacketsRecv uint64
+		totalPacketsSent uint64
+		totalErrIn       uint64
+		totalErrOut      uint64
+		totalDropIn      uint64
+		totalDropOut     uint64
+		totalFifoIn      uint64
+		totalFifoOut     uint64
+	)
+
+	// Create a map of external interface names
+	externalInterfaces := make(map[string]bool)
+
+	// Identify external interfaces
+	for _, iface := range interfaces {
+		// Skip down or loopback interfaces
+		if iface.Flags&stdnet.FlagUp == 0 ||
+			iface.Flags&stdnet.FlagLoopback != 0 {
+			continue
+		}
+
+		// Get addresses for this interface
+		addrs, err := iface.Addrs()
+		if err != nil {
+			logger.Error(err)
+			continue
+		}
+
+		// Skip interfaces without addresses
+		if len(addrs) == 0 {
+			continue
+		}
+
+		// Check for non-private IP addresses
+		for _, addr := range addrs {
+			ip, ipNet, err := stdnet.ParseCIDR(addr.String())
+			if err != nil {
+				continue
+			}
+
+			// Skip virtual, local, multicast, and special purpose IPs
+			if !isRealExternalIP(ip, ipNet) {
+				continue
+			}
+
+			externalInterfaces[iface.Name] = true
+			break
+		}
+	}
+
+	// Accumulate stats only from external interfaces
+	for _, stat := range networkStats {
+		if externalInterfaces[stat.Name] {
+			totalBytesRecv += stat.BytesRecv
+			totalBytesSent += stat.BytesSent
+			totalPacketsRecv += stat.PacketsRecv
+			totalPacketsSent += stat.PacketsSent
+			totalErrIn += stat.Errin
+			totalErrOut += stat.Errout
+			totalDropIn += stat.Dropin
+			totalDropOut += stat.Dropout
+			totalFifoIn += stat.Fifoin
+			totalFifoOut += stat.Fifoout
+		}
+	}
+
+	return &net.IOCountersStat{
+		Name:        "analytic.network",
+		BytesRecv:   totalBytesRecv,
+		BytesSent:   totalBytesSent,
+		PacketsRecv: totalPacketsRecv,
+		PacketsSent: totalPacketsSent,
+		Errin:       totalErrIn,
+		Errout:      totalErrOut,
+		Dropin:      totalDropIn,
+		Dropout:     totalDropOut,
+		Fifoin:      totalFifoIn,
+		Fifoout:     totalFifoOut,
+	}, nil
+}
+
+// isRealExternalIP checks if an IP is a genuine external (public) IP
+func isRealExternalIP(ip stdnet.IP, ipNet *stdnet.IPNet) bool {
+	// Skip if it's not a global unicast address
+	if !ip.IsGlobalUnicast() {
+		return false
+	}
+
+	// Skip private IPs
+	if ip.IsPrivate() {
+		return false
+	}
+
+	// Skip link-local addresses
+	if ip.IsLinkLocalUnicast() {
+		return false
+	}
+
+	// Skip loopback
+	if ip.IsLoopback() {
+		return false
+	}
+
+	// Skip multicast
+	if ip.IsMulticast() {
+		return false
+	}
+
+	// Check for special reserved ranges
+	if isReservedIP(ip) {
+		return false
+	}
+
+	return true
+}
+
+// isReservedIP checks if an IP belongs to special reserved ranges
+func isReservedIP(ip stdnet.IP) bool {
+	// Handle IPv4
+	if ip4 := ip.To4(); ip4 != nil {
+		// TEST-NET-1: 192.0.2.0/24 (RFC 5737)
+		if ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 2 {
+			return true
+		}
+
+		// TEST-NET-2: 198.51.100.0/24 (RFC 5737)
+		if ip4[0] == 198 && ip4[1] == 51 && ip4[2] == 100 {
+			return true
+		}
+
+		// TEST-NET-3: 203.0.113.0/24 (RFC 5737)
+		if ip4[0] == 203 && ip4[1] == 0 && ip4[2] == 113 {
+			return true
+		}
+
+		// Benchmark tests: 198.18.0.0/15 (includes 198.19.0.0/16) (RFC 2544)
+		if ip4[0] == 198 && (ip4[1] == 18 || ip4[1] == 19) {
+			return true
+		}
+
+		// Documentation: 240.0.0.0/4 (RFC 1112)
+		if ip4[0] >= 240 {
+			return true
+		}
+
+		// CGNAT: 100.64.0.0/10 (RFC 6598)
+		if ip4[0] == 100 && (ip4[1]&0xC0) == 64 {
+			return true
+		}
+	} else if ip.To16() != nil {
+		// Documentation prefix (2001:db8::/32) - RFC 3849
+		if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x0d && ip[3] == 0xb8 {
+			return true
+		}
+
+		// Unique Local Addresses (fc00::/7) - RFC 4193
+		if (ip[0] & 0xfe) == 0xfc {
+			return true
+		}
+
+		// 6to4 relay (2002::/16) - RFC 3056
+		if ip[0] == 0x20 && ip[1] == 0x02 {
+			return true
+		}
+
+		// Teredo tunneling (2001:0::/32) - RFC 4380
+		if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 {
+			return true
+		}
+
+		// Deprecated site-local addresses (fec0::/10) - RFC 3879
+		if (ip[0]&0xff) == 0xfe && (ip[1]&0xc0) == 0xc0 {
+			return true
+		}
+
+		// Old 6bone addresses (3ffe::/16) - Deprecated
+		if ip[0] == 0x3f && ip[1] == 0xfe {
+			return true
+		}
+
+		// ORCHID addresses (2001:10::/28) - RFC 4843
+		if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && (ip[3]&0xf0) == 0x10 {
+			return true
+		}
+
+		// ORCHID v2 addresses (2001:20::/28) - RFC 7343
+		if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && (ip[3]&0xf0) == 0x20 {
+			return true
+		}
+	}
+
+	return false
+}

+ 6 - 11
internal/analytic/node_stat.go

@@ -1,13 +1,13 @@
 package analytic
 
 import (
-	"github.com/shirou/gopsutil/v4/cpu"
-	"github.com/shirou/gopsutil/v4/load"
-	"github.com/shirou/gopsutil/v4/net"
-	"github.com/uozi-tech/cosy/logger"
 	"math"
 	"runtime"
 	"time"
+
+	"github.com/shirou/gopsutil/v4/cpu"
+	"github.com/shirou/gopsutil/v4/load"
+	"github.com/uozi-tech/cosy/logger"
 )
 
 func GetNodeStat() (data NodeStat) {
@@ -37,22 +37,17 @@ func GetNodeStat() (data NodeStat) {
 		return
 	}
 
-	netIO, err := net.IOCounters(false)
+	network, err := GetNetworkStat()
 	if err != nil {
 		logger.Error(err)
 		return
 	}
 
-	var network net.IOCountersStat
-	if len(netIO) > 0 {
-		network = netIO[0]
-	}
-
 	return NodeStat{
 		AvgLoad:       loadAvg,
 		CPUPercent:    math.Min((cpuUserUsage+cpuSystemUsage)*100, 100),
 		MemoryPercent: memory.Pressure,
 		DiskPercent:   diskStat.Percentage,
-		Network:       network,
+		Network:       *network,
 	}
 }

+ 5 - 72
internal/analytic/record.go

@@ -1,13 +1,11 @@
 package analytic
 
 import (
-	stdnet "net"
 	"runtime"
 	"time"
 
 	"github.com/shirou/gopsutil/v4/cpu"
 	"github.com/shirou/gopsutil/v4/disk"
-	"github.com/shirou/gopsutil/v4/net"
 	"github.com/uozi-tech/cosy/logger"
 )
 
@@ -71,87 +69,22 @@ func recordCpu(now time.Time) {
 
 func recordNetwork(now time.Time) {
 	// Get separate statistics for each interface
-	networkStats, err := net.IOCounters(true)
+	networkStats, err := GetNetworkStat()
 	if err != nil {
 		logger.Error(err)
 		return
 	}
 
-	if len(networkStats) == 0 {
-		return
-	}
-
-	// Get all network interfaces
-	interfaces, err := stdnet.Interfaces()
-	if err != nil {
-		logger.Error(err)
-		return
-	}
-
-	var totalBytesRecv uint64
-	var totalBytesSent uint64
-	var externalInterfaceFound bool
-
-	// Iterate through all interfaces to find external ones
-	for _, iface := range interfaces {
-		// Skip interfaces that are down
-		if iface.Flags&stdnet.FlagUp == 0 {
-			continue
-		}
-
-		// Get IP addresses for the interface
-		addrs, err := iface.Addrs()
-		if err != nil {
-			logger.Error(err)
-			continue
-		}
-
-		// Check if this is an external interface
-		for _, addr := range addrs {
-			if ipNet, ok := addr.(*stdnet.IPNet); ok {
-				// Exclude loopback addresses and private IPs
-				if !ipNet.IP.IsLoopback() {
-					// Found external interface, accumulate its statistics
-					for _, stat := range networkStats {
-						if stat.Name == iface.Name {
-							totalBytesRecv += stat.BytesRecv
-							totalBytesSent += stat.BytesSent
-							externalInterfaceFound = true
-							break
-						}
-					}
-					break
-				}
-			}
-		}
-	}
-
-	// If no external interface is found, use fallback option
-	if !externalInterfaceFound {
-		// Fallback: use all non-loopback interfaces
-		for _, iface := range interfaces {
-			if iface.Flags&stdnet.FlagLoopback == 0 && iface.Flags&stdnet.FlagUp != 0 {
-				for _, stat := range networkStats {
-					if stat.Name == iface.Name {
-						totalBytesRecv += stat.BytesRecv
-						totalBytesSent += stat.BytesSent
-						break
-					}
-				}
-			}
-		}
-	}
-
-	LastNetRecv = totalBytesRecv
-	LastNetSent = totalBytesSent
+	LastNetRecv = networkStats.BytesRecv
+	LastNetSent = networkStats.BytesSent
 
 	NetRecvRecord = append(NetRecvRecord, Usage[uint64]{
 		Time:  now,
-		Usage: totalBytesRecv - LastNetRecv,
+		Usage: networkStats.BytesRecv - LastNetRecv,
 	})
 	NetSentRecord = append(NetSentRecord, Usage[uint64]{
 		Time:  now,
-		Usage: totalBytesSent - LastNetSent,
+		Usage: networkStats.BytesSent - LastNetSent,
 	})
 
 	if len(NetRecvRecord) > 100 {

+ 7 - 6
test/analytic_test.go

@@ -2,14 +2,15 @@ package test
 
 import (
 	"fmt"
+	"runtime"
+	"testing"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/analytic"
 	"github.com/shirou/gopsutil/v4/cpu"
 	"github.com/shirou/gopsutil/v4/disk"
 	"github.com/shirou/gopsutil/v4/load"
 	"github.com/shirou/gopsutil/v4/mem"
-	"github.com/shirou/gopsutil/v4/net"
-	"runtime"
-	"testing"
-	"time"
 )
 
 func TestGoPsutil(t *testing.T) {
@@ -35,9 +36,9 @@ func TestGoPsutil(t *testing.T) {
 	diskUsage, _ := disk.Usage(".")
 	fmt.Println(diskUsage.String())
 
-	network, _ := net.IOCounters(false)
+	network, _ := analytic.GetNetworkStat()
 	fmt.Println(network)
 	time.Sleep(time.Second)
-	network, _ = net.IOCounters(false)
+	network, _ = analytic.GetNetworkStat()
 	fmt.Println(network)
 }