- 新增 collector/system.go: SystemCollector(OS/内核/ulimit) + ProcessCollector(Top 5 CPU/内存) - 模型新增 SystemStat + ProcessStat + ProcessInfo - Report 结构新增 System + Processes 字段 - Agent 启动时采集系统信息,每周期采集进程列表
218 lines
4.8 KiB
Go
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
|
|
}
|