Files
aiops/agent/main.go
T
attacker 5949ec5d0c feat: Agent采集uptime, 后端持久化kernel/uptime/diskIO, 前端磁盘BPS+IOPS展示
- Agent: system.go 添加 readUptime(), SystemStat 携带 Uptime
- Backend: Server 模型新增 Kernel 字段, ReceiveReport 持久化 kernel+uptime
- Backend: /agent/latest 返回 kernel/uptime/port_info/disk_io_info/disk_total
- Frontend: 磁盘监控改为 BPS+IOPS (4张卡片), 系统信息修复内核/运行时长/监听端口/磁盘容量
- 图表单位从 MB/s 改为 KB/s, 添加 formatBPS 工具函数
2026-06-19 01:10:00 +00:00

266 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package main 是 AIOps Agent 的入口。
// Agent 定时采集 CPU、内存、磁盘及监听端口指标,并通过 HTTP 上报至 AIOps 后端。
//
// 以下变量可在编译时通过 -ldflags "-X main.version=... -X main.buildTime=..." 注入。
//
// 署名 Logan.Li
package main
import (
"context"
"flag"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"ezops/agent/collector"
"ezops/agent/model"
"ezops/agent/reporter"
"ezops/agent/task"
)
// 编译时可注入的版本信息
var (
version = "dev"
buildTime = "unknown"
)
func main() {
server := flag.String("server", "http://localhost:8080", "AIOps 后端上报地址(含 scheme 与端口)")
interval := flag.Int("interval", 10, "采集与上报间隔,单位:秒")
token := flag.String("token", "", "Bearer 鉴权令牌")
hostnameFlag := flag.String("hostname", "", "上报主机名,默认使用系统主机名")
flag.Parse()
// 统一日志格式:带微秒时间戳,便于排查问题
logger := log.New(os.Stdout, "[aiops-agent] ", log.LstdFlags|log.Lmicroseconds)
// 解析主机名:优先使用 -hostname,否则回退到 os.Hostname()
hostname := *hostnameFlag
if hostname == "" {
if h, err := os.Hostname(); err == nil {
hostname = h
} else {
hostname = "unknown"
}
}
// 参数校验
if *server == "" {
logger.Fatal("参数 -server 不能为空")
}
if *interval < 1 {
logger.Fatalf("参数 -interval 必须 >= 1,当前为 %d", *interval)
}
logger.Printf("AIOps Agent 启动: version=%s buildTime=%s", version, buildTime)
logger.Printf("配置: server=%s interval=%ds hostname=%q token=%s",
*server, *interval, hostname, maskToken(*token))
// 初始化各采集器与上报器
cpuCollector := collector.NewCPUCollector()
memCollector := collector.NewMemCollector()
diskCollector := collector.NewDiskCollector()
portCollector := collector.NewPortCollector()
netCollector := collector.NewNetworkCollector()
diskIOCollector := collector.NewDiskIOCollector()
rep := reporter.New(*server, *token, logger)
// 采集系统静态信息(OS/内核),仅在启动时执行一次
sysInfo := collector.CollectSystem()
logger.Printf("系统信息: OS=%q Kernel=%q", sysInfo.OSVersion, sysInfo.Kernel)
// 探测本机主 IP(用于后端匹配 servers 表)
localIP := detectLocalIP()
logger.Printf("探测本机 IP: %s", localIP)
// 系统信息上报计数器:前 3 次上报携带系统信息,确保后端至少收到一次
sysReportCount := 0
// 初始化任务执行器与任务客户端(与 reporter 复用相同的 server/token/hostname)
taskExecutor := task.New(logger)
taskClient := task.NewClient(*server, *token, hostname, logger)
// 监听 SIGINT / SIGTERM,支持优雅退出
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ticker := time.NewTicker(time.Duration(*interval) * time.Second)
defer ticker.Stop()
// collectAndReport 执行单次采集并上报;各项采集独立进行,单项失败不影响整体上报
collectAndReport := func() {
report := &model.Report{
Hostname: hostname,
IP: localIP,
Timestamp: time.Now().UTC().Format(time.RFC3339),
AgentVersion: version,
}
// 前 3 次上报携带系统信息
if sysReportCount < 3 && (sysInfo.OSVersion != "" || sysInfo.Kernel != "") {
report.System = &model.SystemStat{
OSVersion: sysInfo.OSVersion,
Kernel: sysInfo.Kernel,
Uptime: sysInfo.UptimeSeconds,
}
sysReportCount++
}
if stat, err := cpuCollector.Collect(); err != nil {
logger.Printf("CPU 采集异常: %v", err)
} else {
report.CPU = stat
}
if stat, err := memCollector.Collect(); err != nil {
logger.Printf("内存采集异常: %v", err)
} else {
report.Memory = stat
}
if disks, err := diskCollector.Collect(); err != nil {
logger.Printf("磁盘采集异常: %v", err)
} else {
report.Disks = disks
}
if ports, err := portCollector.Collect(); err != nil {
logger.Printf("端口采集异常: %v", err)
} else {
report.Ports = ports
}
if netStat, err := netCollector.Collect(); err != nil {
logger.Printf("网络采集异常: %v", err)
} else {
report.Network = netStat
}
if dioStat, err := diskIOCollector.Collect(); err != nil {
logger.Printf("磁盘IO采集异常: %v", err)
} else {
report.DiskIO = dioStat
}
if err := rep.Send(ctx, report); err != nil {
logger.Printf("上报失败: %v", err)
return
}
logger.Printf("上报成功: cpu=%.2f%% mem=%.2f%% disks=%d ports=%d net=%dconn/%dwait netIO=%.0f/%.0fB/s diskIO=%.0f/%.0fB/s",
report.CPU.UsagePercent, report.Memory.UsagePercent, len(report.Disks), len(report.Ports),
report.Network.TCPEstablished, report.Network.TCPTimeWait,
report.Network.NetRxBPS, report.Network.NetTxBPS,
report.DiskIO.ReadBPS, report.DiskIO.WriteBPS)
}
// pollAndExecute 执行单次任务轮询:拉取待执行任务 → 逐个执行 → 回传结果。
// 网络错误只记录日志,不 panic,下一个周期继续重试。
pollAndExecute := func() {
tasks, err := taskClient.Poll(ctx)
if err != nil {
// 上下文已取消(Agent 退出中)属于正常情况,不输出告警
if ctx.Err() != nil {
return
}
logger.Printf("任务轮询失败: %v", err)
return
}
if len(tasks) == 0 {
return // 暂无任务,静默返回
}
logger.Printf("拉取到 %d 个待执行任务", len(tasks))
for _, t := range tasks {
// Agent 退出时跳过后续任务,避免退出延迟
if ctx.Err() != nil {
return
}
result := taskExecutor.ExecuteTask(ctx, t)
if err := taskClient.Submit(ctx, result); err != nil {
if ctx.Err() != nil {
return
}
logger.Printf("任务 %s 结果回传失败: %v", t.TaskID, err)
} else {
logger.Printf("任务 %s 结果回传成功 status=%s", t.TaskID, result.Status)
}
}
}
// 启动任务轮询协程:每 5 秒轮询一次,与指标采集并行运行。
// 使用独立的 5 秒固定间隔,不受 -interval 影响。
const taskPollInterval = 5 * time.Second
go func() {
// 启动后立即轮询一次,再按固定周期执行
pollAndExecute()
ticker := time.NewTicker(taskPollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
pollAndExecute()
case <-ctx.Done():
return
}
}
}()
// 主协程:启动后立即执行一次指标采集,再按 ticker 周期执行
collectAndReport()
for {
select {
case <-ticker.C:
collectAndReport()
case <-ctx.Done():
logger.Println("收到退出信号,Agent 正在关闭...")
return
}
}
}
// maskToken 对令牌做脱敏处理,仅用于日志输出
func maskToken(token string) string {
if token == "" {
return "(empty)"
}
if len(token) <= 4 {
return "****"
}
return token[:4] + "****"
}
// detectLocalIP 探测本机主 IP 地址。
// 通过 UDP 连接到公网地址(不实际发包),获取系统路由选择的本机 IP。
// 如果失败,回退到遍历网卡查找非 loopback 的 IPv4 地址。
func detectLocalIP() string {
// 方法1:通过 UDP "连接" 到公网 DNS 获取出口 IP
conn, err := net.Dial("udp", "8.8.8.8:53")
if err == nil {
defer conn.Close()
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok && addr.IP != nil {
return addr.IP.String()
}
}
// 方法2:遍历网卡
ifaces, err := net.Interfaces()
if err == nil {
for _, iface := range ifaces {
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
continue
}
addrs, _ := iface.Addrs()
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
}
return "unknown"
}