Files
aiops/agent/collector/system.go
T
attacker 06d7451cf9 feat: Phase 6 Agent 系统信息+进程监控采集器
- 新增 collector/system.go: SystemCollector(OS/内核/ulimit) + ProcessCollector(Top 5 CPU/内存)
- 模型新增 SystemStat + ProcessStat + ProcessInfo
- Report 结构新增 System + Processes 字段
- Agent 启动时采集系统信息,每周期采集进程列表
2026-06-18 13:03:14 +00:00

218 lines
4.8 KiB
Go

// Package collector 提供系统信息及进程列表采集。
// 系统信息:OS 名称、内核版本、架构、ulimit。
// 进程列表:按 CPU/内存排序的 Top 5 进程。
//
// 署名 Logan.Li
package collector
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"syscall"
"ezops/agent/model"
)
// SystemCollector 系统信息采集器
type SystemCollector struct{}
// NewSystemCollector 创建系统信息采集器
func NewSystemCollector() *SystemCollector {
return &SystemCollector{}
}
// Collect 采集系统信息(OS、内核、架构、ulimit)
func (s *SystemCollector) Collect() (model.SystemStat, error) {
return model.SystemStat{
OS: readFirstLine("/etc/os-release", "PRETTY_NAME="),
Kernel: readProcVersion(),
Arch: readArch(),
Hostname: readHostname(),
UlimitNofile: readUlimitNofile(),
UlimitNproc: readUlimitNproc(),
}, nil
}
func readFirstLine(path, prefix string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, prefix) {
val := strings.TrimPrefix(line, prefix)
return strings.Trim(val, `"`)
}
}
return ""
}
func readProcVersion() string {
data, err := os.ReadFile("/proc/version")
if err != nil {
return ""
}
// Linux version 6.8.0-90-generic ... (buildd@...) (gcc ...) #...
parts := strings.Fields(string(data))
if len(parts) >= 3 {
return strings.Join(parts[:3], " ")
}
return strings.TrimSpace(string(data))
}
func readArch() string {
data, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "flags") || strings.HasPrefix(line, "Features") {
return "x86_64"
}
}
return "unknown"
}
func readHostname() string {
h, _ := os.Hostname()
return h
}
func readUlimitNofile() int64 {
var rlim syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim); err == nil {
return int64(rlim.Cur)
}
return 0
}
func readUlimitNproc() int64 {
var rlim syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NPROC, &rlim); err == nil {
return int64(rlim.Cur)
}
return 0
}
// ==================== Process Collector ====================
// ProcessCollector 进程列表采集器 — Top N 按 CPU/内存
type ProcessCollector struct{}
// NewProcessCollector 创建进程采集器
func NewProcessCollector() *ProcessCollector {
return &ProcessCollector{}
}
// Collect 采集 Top 5 CPU 和 Top 5 内存进程
func (p *ProcessCollector) Collect() (model.ProcessStat, error) {
procs := readProcStatAll()
if len(procs) == 0 {
return model.ProcessStat{}, nil
}
// Top 5 by CPU
sort.Slice(procs, func(i, j int) bool { return procs[i].CPUPercent > procs[j].CPUPercent })
topCPU := procs
if len(topCPU) > 5 {
topCPU = topCPU[:5]
}
// Top 5 by RSS
sort.Slice(procs, func(i, j int) bool { return procs[i].RSSKB > procs[j].RSSKB })
topMem := procs
if len(topMem) > 5 {
topMem = topMem[:5]
}
return model.ProcessStat{
TopCPU: topCPU,
TopMem: topMem,
}, nil
}
type procInfo struct {
PID int
Name string
CPUPercent float64
RSSKB int64
}
func readProcStatAll() []procInfo {
entries, err := os.ReadDir("/proc")
if err != nil {
return nil
}
var procs []procInfo
for _, entry := range entries {
if !entry.IsDir() {
continue
}
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
statData, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
continue
}
stat := string(statData)
// /proc/PID/stat: PID (comm) state ppid ...
// comm 在括号中,可能含空格
closeParen := strings.LastIndex(stat, ")")
if closeParen < 0 {
continue
}
openParen := strings.Index(stat, "(")
if openParen < 0 {
continue
}
name := stat[openParen+1 : closeParen]
fields := strings.Fields(stat[closeParen+2:])
if len(fields) < 20 {
continue
}
// utime=fields[11], stime=fields[12], rss=fields[21]
utime, _ := strconv.ParseInt(fields[11], 10, 64)
stime, _ := strconv.ParseInt(fields[12], 10, 64)
rss, _ := strconv.ParseInt(fields[21], 10, 64)
// RSS pages * 4KB = RSS in KB
rssKB := rss * 4
// Simple CPU%: total ticks as fraction of uptime (approximate)
totalTicks := utime + stime
uptimeData, _ := os.ReadFile("/proc/uptime")
uptime := 0.0
if uptimeData != nil {
uptime, _ = strconv.ParseFloat(strings.Fields(string(uptimeData))[0], 64)
}
hertz := float64(syscall.Getpagesize()) * 25
if hertz == 0 {
hertz = 100
}
// CLK_TCK is typically 100 on Linux
cpuPercent := 0.0
if uptime > 0 {
cpuPercent = float64(totalTicks) / 100.0 / uptime * 100
}
procs = append(procs, procInfo{
PID: pid,
Name: name,
CPUPercent: cpuPercent,
RSSKB: rssKB,
})
}
return procs
}