system.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. package system
  2. import (
  3. "bufio"
  4. "fmt"
  5. "math"
  6. "os"
  7. "runtime"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/vishvananda/netlink"
  13. )
  14. type SystemStatus struct {
  15. Hostname string `json:"hostname"`
  16. OS string `json:"os"`
  17. Kernel string `json:"kernel"`
  18. UptimeSeconds float64 `json:"uptime_seconds"`
  19. UptimeHuman string `json:"uptime_human"`
  20. CPUPct float64 `json:"cpu_pct"` // Actual container CPU utilization % (0-100)
  21. CPULoad []string `json:"cpu_load"` // 1m, 5m, 15m host load average
  22. CPUCores int `json:"cpu_cores"`
  23. MemTotalMB uint64 `json:"mem_total_mb"`
  24. MemUsedMB uint64 `json:"mem_used_mb"`
  25. MemUsagePct float64 `json:"mem_usage_pct"`
  26. TotalIfaces int `json:"total_ifaces"`
  27. UpIfaces int `json:"up_ifaces"`
  28. DownIfaces int `json:"down_ifaces"`
  29. VlanIfaces int `json:"vlan_ifaces"`
  30. DefaultGateway string `json:"default_gateway"`
  31. }
  32. type RouteInfo struct {
  33. Destination string `json:"destination"`
  34. Gateway string `json:"gateway"`
  35. Interface string `json:"interface"`
  36. Protocol int `json:"protocol"`
  37. Scope int `json:"scope"`
  38. }
  39. // GetStatus returns system-level metrics and router telemetry.
  40. func GetStatus() (*SystemStatus, error) {
  41. hostname, _ := os.Hostname()
  42. osName := "Debian Linux"
  43. if f, err := os.Open("/etc/os-release"); err == nil {
  44. scanner := bufio.NewScanner(f)
  45. for scanner.Scan() {
  46. line := scanner.Text()
  47. if strings.HasPrefix(line, "PRETTY_NAME=") {
  48. osName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
  49. break
  50. }
  51. }
  52. _ = f.Close()
  53. }
  54. kernel := "Linux"
  55. if data, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil {
  56. kernel = strings.TrimSpace(string(data))
  57. }
  58. uptimeSec, uptimeHuman := getUptime()
  59. cpuLoads := getCPULoad()
  60. cpuPct := getContainerCPUUsage()
  61. memTotal, memUsed, memPct := getMemory()
  62. defGateway := getDefaultGateway()
  63. return &SystemStatus{
  64. Hostname: hostname,
  65. OS: osName,
  66. Kernel: kernel,
  67. UptimeSeconds: uptimeSec,
  68. UptimeHuman: uptimeHuman,
  69. CPUPct: cpuPct,
  70. CPULoad: cpuLoads,
  71. CPUCores: runtime.NumCPU(),
  72. MemTotalMB: memTotal,
  73. MemUsedMB: memUsed,
  74. MemUsagePct: memPct,
  75. DefaultGateway: defGateway,
  76. }, nil
  77. }
  78. var (
  79. lastUsageUsec uint64
  80. lastUsageTime time.Time
  81. lastUsageMu sync.Mutex
  82. containerCPUPct float64
  83. )
  84. func getContainerCPUUsage() float64 {
  85. lastUsageMu.Lock()
  86. defer lastUsageMu.Unlock()
  87. // Measure container CPU utilization via cgroup v2
  88. if f, err := os.Open("/sys/fs/cgroup/cpu.stat"); err == nil {
  89. scanner := bufio.NewScanner(f)
  90. var usageUsec uint64
  91. for scanner.Scan() {
  92. line := scanner.Text()
  93. if strings.HasPrefix(line, "usage_usec ") {
  94. parts := strings.Fields(line)
  95. if len(parts) >= 2 {
  96. usageUsec, _ = strconv.ParseUint(parts[1], 10, 64)
  97. }
  98. break
  99. }
  100. }
  101. _ = f.Close()
  102. now := time.Now()
  103. if lastUsageTime.IsZero() {
  104. lastUsageUsec = usageUsec
  105. lastUsageTime = now
  106. return 0.5
  107. }
  108. elapsedSec := now.Sub(lastUsageTime).Seconds()
  109. if elapsedSec >= 0.5 && usageUsec >= lastUsageUsec {
  110. deltaUsec := float64(usageUsec - lastUsageUsec)
  111. pct := (deltaUsec / (elapsedSec * 1000000.0)) * 100.0
  112. if pct < 0 {
  113. pct = 0
  114. }
  115. if pct > 100 {
  116. pct = 100
  117. }
  118. containerCPUPct = math.Round(pct*10) / 10
  119. lastUsageUsec = usageUsec
  120. lastUsageTime = now
  121. }
  122. return containerCPUPct
  123. }
  124. return 0.5
  125. }
  126. func getUptime() (float64, string) {
  127. data, err := os.ReadFile("/proc/uptime")
  128. if err != nil {
  129. return 0, "Unknown"
  130. }
  131. parts := strings.Fields(string(data))
  132. if len(parts) == 0 {
  133. return 0, "Unknown"
  134. }
  135. sec, _ := strconv.ParseFloat(parts[0], 64)
  136. days := int(sec) / 86400
  137. hours := (int(sec) % 86400) / 3600
  138. minutes := (int(sec) % 3600) / 60
  139. if days > 0 {
  140. return sec, fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
  141. }
  142. return sec, fmt.Sprintf("%dh %dm", hours, minutes)
  143. }
  144. func getCPULoad() []string {
  145. data, err := os.ReadFile("/proc/loadavg")
  146. if err != nil {
  147. return []string{"0.00", "0.00", "0.00"}
  148. }
  149. parts := strings.Fields(string(data))
  150. if len(parts) >= 3 {
  151. return parts[:3]
  152. }
  153. return []string{"0.00", "0.00", "0.00"}
  154. }
  155. func getMemory() (totalMB uint64, usedMB uint64, pct float64) {
  156. f, err := os.Open("/proc/meminfo")
  157. if err != nil {
  158. return 0, 0, 0
  159. }
  160. defer f.Close()
  161. var total, available uint64
  162. scanner := bufio.NewScanner(f)
  163. for scanner.Scan() {
  164. line := scanner.Text()
  165. if strings.HasPrefix(line, "MemTotal:") {
  166. parts := strings.Fields(line)
  167. if len(parts) >= 2 {
  168. total, _ = strconv.ParseUint(parts[1], 10, 64)
  169. }
  170. } else if strings.HasPrefix(line, "MemAvailable:") {
  171. parts := strings.Fields(line)
  172. if len(parts) >= 2 {
  173. available, _ = strconv.ParseUint(parts[1], 10, 64)
  174. }
  175. }
  176. }
  177. totalMB = total / 1024
  178. usedMB = (total - available) / 1024
  179. if total > 0 {
  180. pct = (float64(total-available) / float64(total)) * 100.0
  181. }
  182. return
  183. }
  184. func getDefaultGateway() string {
  185. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  186. if err != nil {
  187. return ""
  188. }
  189. for _, r := range routes {
  190. if r.Dst == nil && r.Gw != nil {
  191. return r.Gw.String()
  192. }
  193. }
  194. return ""
  195. }
  196. // GetRoutingTable returns the list of active routes.
  197. func GetRoutingTable() ([]RouteInfo, error) {
  198. routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
  199. if err != nil {
  200. return nil, err
  201. }
  202. links, _ := netlink.LinkList()
  203. linkMap := make(map[int]string)
  204. for _, l := range links {
  205. linkMap[l.Attrs().Index] = l.Attrs().Name
  206. }
  207. result := make([]RouteInfo, 0)
  208. for _, r := range routes {
  209. dst := "default"
  210. if r.Dst != nil {
  211. dst = r.Dst.String()
  212. }
  213. gw := ""
  214. if r.Gw != nil {
  215. gw = r.Gw.String()
  216. }
  217. result = append(result, RouteInfo{
  218. Destination: dst,
  219. Gateway: gw,
  220. Interface: linkMap[r.LinkIndex],
  221. Protocol: int(r.Protocol),
  222. Scope: int(r.Scope),
  223. })
  224. }
  225. return result, nil
  226. }