From 06d7451cf9ef2fc84a2a0e53bdab664e82326276 Mon Sep 17 00:00:00 2001 From: attacker Date: Thu, 18 Jun 2026 13:03:14 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=206=20Agent=20=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E4=BF=A1=E6=81=AF+=E8=BF=9B=E7=A8=8B=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E9=87=87=E9=9B=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 collector/system.go: SystemCollector(OS/内核/ulimit) + ProcessCollector(Top 5 CPU/内存) - 模型新增 SystemStat + ProcessStat + ProcessInfo - Report 结构新增 System + Processes 字段 - Agent 启动时采集系统信息,每周期采集进程列表 --- agent/collector/system.go | 217 ++++++++++++++++++++++++++++++++++++++ agent/main.go | 22 ++++ agent/model/model.go | 26 +++++ 3 files changed, 265 insertions(+) create mode 100644 agent/collector/system.go diff --git a/agent/collector/system.go b/agent/collector/system.go new file mode 100644 index 0000000..d5a5c36 --- /dev/null +++ b/agent/collector/system.go @@ -0,0 +1,217 @@ +// 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 +} diff --git a/agent/main.go b/agent/main.go index ab1ca54..22d7b39 100644 --- a/agent/main.go +++ b/agent/main.go @@ -77,6 +77,18 @@ func main() { diskIOCollector := collector.NewDiskIOCollector() rep := reporter.New(*server, *token, logger) + // 系统信息(仅启动时采集一次) + sysCollector := collector.NewSystemCollector() + var systemStat model.SystemStat + if stat, err := sysCollector.Collect(); err == nil { + systemStat = stat + logger.Printf("系统信息: OS=%s Kernel=%s Arch=%s nofile=%d nproc=%d", + stat.OS, stat.Kernel, stat.Arch, stat.UlimitNofile, stat.UlimitNproc) + } + + // 进程采集器 + procCollector := collector.NewProcessCollector() + // 中间件采集器(可选) var mwCollector *collector.MiddlewareCollector if *redisFlag != "" || *mysqlFlag != "" { @@ -154,6 +166,16 @@ func main() { } } + // 系统信息(每次上报附带) + report.System = systemStat + + // 进程采集 + if procStat, err := procCollector.Collect(); err != nil { + logger.Printf("进程采集异常: %v", err) + } else { + report.Processes = procStat + } + if err := rep.Send(ctx, report); err != nil { logger.Printf("上报失败: %v", err) return diff --git a/agent/model/model.go b/agent/model/model.go index 692974e..a39f215 100644 --- a/agent/model/model.go +++ b/agent/model/model.go @@ -15,6 +15,8 @@ type Report struct { Network NetworkStat `json:"network"` // 网络指标 DiskIO DiskIOStat `json:"disk_io"` // 磁盘 IO 指标 Middleware MiddlewareStat `json:"middleware,omitempty"` // 中间件指标(Redis/MySQL) + System SystemStat `json:"system,omitempty"` // 系统信息(OS/内核/ulimit) + Processes ProcessStat `json:"processes,omitempty"` // Top N 进程 } // MiddlewareStat 中间件综合指标 — 包含 Redis + MySQL @@ -79,3 +81,27 @@ type PortStat struct { Process string `json:"process"` // 进程名(可执行文件名) PID int `json:"pid"` // 进程 PID(无法解析时为 0) } + +// SystemStat 系统信息指标 +type SystemStat struct { + OS string `json:"os"` // 操作系统名称 + Kernel string `json:"kernel"` // 内核版本 + Arch string `json:"arch"` // 系统架构 + Hostname string `json:"hostname"` // 主机名 + UlimitNofile int64 `json:"ulimit_nofile"` // 文件句柄数上限 + UlimitNproc int64 `json:"ulimit_nproc"` // 进程数上限 +} + +// ProcessStat 进程监控指标 +type ProcessStat struct { + TopCPU []ProcessInfo `json:"top_cpu"` // Top 5 CPU 进程 + TopMem []ProcessInfo `json:"top_mem"` // Top 5 内存进程 +} + +// ProcessInfo 单个进程信息 +type ProcessInfo struct { + PID int `json:"pid"` // 进程 ID + Name string `json:"name"` // 进程名 + CPUPercent float64 `json:"cpu_percent"` // CPU 使用率 (%) + RSSKB int64 `json:"rss_kb"` // 物理内存 (KB) +}