feat: 完善堡垒机与 Agent 智能运维能力

- 支持 Windows Agent 构建、安装脚本和统一上报协议

- 增强 RDP 剪贴板、虚拟盘和文件传输能力

- 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
Logan.Li committed 2026-07-05 23:49:36 +08:00
1 parent a9513c2056
commit e0d652d211
169 files changed
+12192 -9116

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 的上下文切换采集器。
// 读取 /proc/stat 中的 ctxt(上下文切换)和 intr(中断)累计值。
// 注意:此处返回累计值而非速率,后端可用两次上报差值计算。
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package collector
import "ezops/agent/model"
type ContextSwitchCollector struct{}
func NewContextSwitchCollector() *ContextSwitchCollector {
return &ContextSwitchCollector{}
}
func (c *ContextSwitchCollector) Collect() (*model.ContextSwitchStat, error) {
return nil, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现 CPU 使用率与负载采集,主要通过解析 Linux /proc/stat 与 /proc/loadavg 实现。
//
+77
View File
@@ -0,0 +1,77 @@
//go:build windows
// Package collector 提供 Windows CPU 指标采集能力。 // Logan.Li
package collector
import (
"fmt"
"runtime"
"sync"
"syscall"
"unsafe"
"ezops/agent/model"
)
type filetime struct {
LowDateTime uint32
HighDateTime uint32
}
type cpuSample struct {
total uint64
idle uint64
iowait uint64
}
type CPUCollector struct {
mu sync.Mutex
prev *cpuSample
}
func NewCPUCollector() *CPUCollector {
return &CPUCollector{}
}
func (c *CPUCollector) Collect() (model.CPUStat, error) {
stat := model.CPUStat{CoreCount: runtime.NumCPU()}
cur, err := readCPUSample()
if err != nil {
return stat, fmt.Errorf("读取 Windows CPU 采样失败: %w", err)
}
c.mu.Lock()
defer c.mu.Unlock()
if c.prev != nil {
totalDelta := cur.total - c.prev.total
idleDelta := cur.idle - c.prev.idle
if totalDelta > 0 {
stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0
if stat.UsagePercent < 0 {
stat.UsagePercent = 0
}
}
}
c.prev = &cur
return stat, nil
}
func readCPUSample() (cpuSample, error) {
var idle, kernel, user filetime
proc := syscall.NewLazyDLL("kernel32.dll").NewProc("GetSystemTimes")
ret, _, err := proc.Call(
uintptr(unsafe.Pointer(&idle)),
uintptr(unsafe.Pointer(&kernel)),
uintptr(unsafe.Pointer(&user)),
)
if ret == 0 {
return cpuSample{}, err
}
idleTicks := filetimeToUint64(idle)
kernelTicks := filetimeToUint64(kernel)
userTicks := filetimeToUint64(user)
return cpuSample{total: kernelTicks + userTicks, idle: idleTicks}, nil
}
func filetimeToUint64(ft filetime) uint64 {
return uint64(ft.HighDateTime)<<32 | uint64(ft.LowDateTime)
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现对所有已挂载的真实文件系统执行 statfs,采集磁盘使用情况。
// 使用 golang.org/x/sys/unix 的 Statfs,兼容 Linux 与 macOS。
+52
View File
@@ -0,0 +1,52 @@
//go:build windows
// Package collector 提供 Windows 逻辑磁盘使用率采集能力。 // Logan.Li
package collector
import (
"fmt"
"syscall"
"unsafe"
"ezops/agent/model"
)
type DiskCollector struct{}
func NewDiskCollector() *DiskCollector {
return &DiskCollector{}
}
func (d *DiskCollector) Collect() ([]model.DiskStat, error) {
mask, _, err := syscall.NewLazyDLL("kernel32.dll").NewProc("GetLogicalDrives").Call()
if mask == 0 {
return nil, fmt.Errorf("GetLogicalDrives 失败: %w", err)
}
var disks []model.DiskStat
for i := 0; i < 26; i++ {
if mask&(1<<uint(i)) == 0 {
continue
}
root := string(rune('A'+i)) + `:\`
var freeAvail, totalBytes, totalFree uint64
pathPtr, _ := syscall.UTF16PtrFromString(root)
ret, _, _ := syscall.NewLazyDLL("kernel32.dll").NewProc("GetDiskFreeSpaceExW").Call(
uintptr(unsafe.Pointer(pathPtr)),
uintptr(unsafe.Pointer(&freeAvail)),
uintptr(unsafe.Pointer(&totalBytes)),
uintptr(unsafe.Pointer(&totalFree)),
)
if ret == 0 || totalBytes == 0 {
continue
}
used := totalBytes - totalFree
disks = append(disks, model.DiskStat{
Mount: root,
Device: root,
TotalGB: bytesToGB(totalBytes),
UsedGB: bytesToGB(used),
UsagePercent: float64(used) / float64(totalBytes) * 100.0,
})
}
return disks, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector — 磁盘 IO 指标采集器
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
//
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package collector
import "ezops/agent/model"
type DiskIOCollector struct{}
func NewDiskIOCollector() *DiskIOCollector {
return &DiskIOCollector{}
}
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
return model.DiskIOStat{}, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 的 FD 句柄采集器。
// 读取 /proc/sys/fs/file-nr 和 /proc/sys/fs/file-max 获取系统级文件句柄使用情况。
//
+15
View File
@@ -0,0 +1,15 @@
//go:build windows
package collector
import "ezops/agent/model"
type FDCollector struct{}
func NewFDCollector() *FDCollector {
return &FDCollector{}
}
func (c *FDCollector) Collect() (*model.SocketFDStat, error) {
return nil, nil
}
+9 -1
View File
@@ -12,6 +12,7 @@ import (
"net"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
@@ -193,7 +194,7 @@ func (h *HealthProbeCollector) execCmd(parentCtx context.Context, cfg model.Prob
ctx, cancel := context.WithTimeout(parentCtx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", cfg.ExecCommand)
cmd := execProbeCommand(ctx, cfg.ExecCommand)
output, err := cmd.CombinedOutput()
latencyMs := int(time.Since(start).Milliseconds())
result.LatencyMs = latencyMs
@@ -214,3 +215,10 @@ func (h *HealthProbeCollector) execCmd(parentCtx context.Context, cfg model.Prob
result.Message = fmt.Sprintf("命令执行成功 (%dms)", latencyMs)
return result
}
func execProbeCommand(ctx context.Context, command string) *exec.Cmd {
if runtime.GOOS == "windows" {
return exec.CommandContext(ctx, "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command)
}
return exec.CommandContext(ctx, "sh", "-c", command)
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现内存使用率采集,通过解析 Linux /proc/meminfo 获取。
//
+56
View File
@@ -0,0 +1,56 @@
//go:build windows
// Package collector 提供 Windows 内存指标采集能力。 // Logan.Li
package collector
import (
"fmt"
"syscall"
"unsafe"
"ezops/agent/model"
)
type memoryStatusEx struct {
Length uint32
MemoryLoad uint32
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
AvailPageFile uint64
TotalVirtual uint64
AvailVirtual uint64
AvailExtendedVirtual uint64
}
type MemCollector struct{}
func NewMemCollector() *MemCollector {
return &MemCollector{}
}
func (m *MemCollector) Collect() (model.MemoryStat, error) {
var ms memoryStatusEx
ms.Length = uint32(unsafe.Sizeof(ms))
ret, _, err := syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalMemoryStatusEx").Call(uintptr(unsafe.Pointer(&ms)))
if ret == 0 {
return model.MemoryStat{}, fmt.Errorf("GlobalMemoryStatusEx 失败: %w", err)
}
total := ms.TotalPhys
avail := ms.AvailPhys
used := total - avail
swapTotal := ms.TotalPageFile
swapAvail := ms.AvailPageFile
swapUsed := uint64(0)
if swapTotal > swapAvail {
swapUsed = swapTotal - swapAvail
}
stat := model.MemoryStat{
TotalGB: bytesToGB(total),
UsedGB: bytesToGB(used),
AvailableGB: bytesToGB(avail),
SwapUsedGB: bytesToGB(swapUsed),
UsagePercent: float64(ms.MemoryLoad),
}
return stat, nil
}
+7 -5
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现网络连接数、网卡流量等网络指标采集。
//
@@ -18,11 +20,11 @@ import (
// netSample 保存上一次的网络采样数据,用于计算速率
type netSample struct {
time time.Time
rxBytes uint64
txBytes uint64
rxErrors uint64
txErrors uint64
time time.Time
rxBytes uint64
txBytes uint64
rxErrors uint64
txErrors uint64
}
// NetworkCollector 网络指标采集器
+53
View File
@@ -0,0 +1,53 @@
//go:build windows
// Package collector 提供 Windows 网络连接基础采集能力。 // Logan.Li
package collector
import (
"bytes"
"os/exec"
"strings"
"ezops/agent/model"
)
type NetworkCollector struct{}
func NewNetworkCollector() *NetworkCollector {
return &NetworkCollector{}
}
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
stat := model.NetworkStat{NicName: firstWindowsNIC()}
out, err := exec.Command("netstat", "-ano", "-p", "tcp").Output()
if err != nil {
return stat, nil
}
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 4 || strings.ToUpper(fields[0]) != "TCP" {
continue
}
state := strings.ToUpper(fields[3])
switch state {
case "ESTABLISHED":
stat.TCPEstablished++
case "TIME_WAIT":
stat.TCPTimeWait++
case "CLOSE_WAIT":
stat.TCPCloseWait++
}
stat.TCPTotal++
}
return stat, nil
}
func firstWindowsNIC() string {
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", "Get-NetAdapter | Where-Object {$_.Status -eq 'Up'} | Select-Object -First 1 -ExpandProperty Name").Output()
if err == nil {
if name := strings.TrimSpace(string(bytes.TrimSpace(out))); name != "" {
return name
}
}
return "Ethernet"
}
+3 -1
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 提供服务器各项系统指标的采集能力。
// 本文件实现监听 TCP 端口的自动发现:
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
@@ -35,7 +37,7 @@ var systemProcesses = map[string]bool{
"upowerd": true,
"rtkit-daemon": true,
"switcheroo-contr": true,
"power-profiles-": true, // power-profiles-daemon
"power-profiles-": true, // power-profiles-daemon
"wpa_supplicant": true,
"avahi-daemon": true,
"cron": true,
+94
View File
@@ -0,0 +1,94 @@
//go:build windows
// Package collector 提供 Windows 监听端口发现能力。 // Logan.Li
package collector
import (
"encoding/csv"
"os/exec"
"strconv"
"strings"
"ezops/agent/model"
)
type PortCollector struct {
Policy *model.PortPolicy
}
func NewPortCollector() *PortCollector {
return &PortCollector{}
}
func (p *PortCollector) SetPolicy(policy *model.PortPolicy) {
p.Policy = policy
}
func (p *PortCollector) Collect() ([]model.PortStat, error) {
pidNames := windowsProcessNames()
out, err := exec.Command("netstat", "-ano", "-p", "tcp").Output()
if err != nil {
return []model.PortStat{}, nil
}
seen := map[string]bool{}
var ports []model.PortStat
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) < 5 || strings.ToUpper(fields[0]) != "TCP" || strings.ToUpper(fields[3]) != "LISTENING" {
continue
}
port := parseWindowsPort(fields[1])
if port <= 0 {
continue
}
if p.Policy != nil && p.Policy.IsBlacklistMode() && !p.Policy.InWhitelist(port) {
continue
}
pid, _ := strconv.Atoi(fields[4])
key := strconv.Itoa(port) + "|" + strconv.Itoa(pid)
if seen[key] {
continue
}
seen[key] = true
ports = append(ports, model.PortStat{
Port: port,
Protocol: "tcp",
Process: pidNames[pid],
PID: pid,
})
}
return ports, nil
}
func parseWindowsPort(addr string) int {
addr = strings.Trim(addr, "[]")
idx := strings.LastIndex(addr, ":")
if idx < 0 || idx+1 >= len(addr) {
return 0
}
port, _ := strconv.Atoi(addr[idx+1:])
return port
}
func windowsProcessNames() map[int]string {
out, err := exec.Command("tasklist", "/FO", "CSV", "/NH").Output()
if err != nil {
return map[int]string{}
}
reader := csv.NewReader(strings.NewReader(string(out)))
rows, err := reader.ReadAll()
if err != nil {
return map[int]string{}
}
result := map[int]string{}
for _, row := range rows {
if len(row) < 2 {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(row[1]))
if err == nil {
result[pid] = row[0]
}
}
return result
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector — 进程 TOP 采集器
// 扫描 /proc 获取 TOP N 进程完整信息:PID/User/CMD/CPU%/MEM%/FDs
// 阿里云 ECS 监控风格 — 直观 6 列布局
+62
View File
@@ -0,0 +1,62 @@
//go:build windows
// Package collector 提供 Windows 进程 TOP 基础采集能力。 // Logan.Li
package collector
import (
"encoding/csv"
"os/exec"
"sort"
"strconv"
"strings"
"ezops/agent/model"
)
type ProcessTopCollector struct{}
func NewProcessTopCollector() *ProcessTopCollector {
return &ProcessTopCollector{}
}
func (p *ProcessTopCollector) Collect(topN int) ([]model.ProcessStat, error) {
out, err := exec.Command("tasklist", "/FO", "CSV", "/NH").Output()
if err != nil {
return []model.ProcessStat{}, nil
}
rows, err := csv.NewReader(strings.NewReader(string(out))).ReadAll()
if err != nil {
return []model.ProcessStat{}, nil
}
var procs []model.ProcessStat
for _, row := range rows {
if len(row) < 5 {
continue
}
pid, _ := strconv.Atoi(strings.TrimSpace(row[1]))
memKB := parseWindowsMemKB(row[4])
procs = append(procs, model.ProcessStat{
PID: pid,
Name: row[0],
Cmdline: row[0],
User: "",
MemoryMB: float64(memKB) / 1024.0,
})
}
sort.Slice(procs, func(i, j int) bool {
return procs[i].MemoryMB > procs[j].MemoryMB
})
if topN > len(procs) {
topN = len(procs)
}
if topN < 0 {
topN = 0
}
return procs[:topN], nil
}
func parseWindowsMemKB(s string) int64 {
clean := strings.NewReplacer(",", "", "K", "", "k", "", " ", "").Replace(s)
v, _ := strconv.ParseInt(clean, 10, 64)
return v
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector — Swap 采集器
// 从 /proc/meminfo 读取 SwapTotal / SwapFree
// 仅当系统有 swap 分区时返回有效数据
+26
View File
@@ -0,0 +1,26 @@
//go:build windows
package collector
import "ezops/agent/model"
type SwapCollector struct{}
func NewSwapCollector() *SwapCollector {
return &SwapCollector{}
}
func (s *SwapCollector) Collect() (*model.SwapStat, error) {
return nil, nil
}
func parseUint64(s string) (uint64, error) {
var v uint64
for _, c := range s {
if c < '0' || c > '9' {
break
}
v = v*10 + uint64(c-'0')
}
return v, nil
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
// Package collector 实现 AIOps Agent 系统静态信息采集。
// 仅在 Agent 启动时执行一次,采集 OS 版本、内核版本等信息。
//
+56
View File
@@ -0,0 +1,56 @@
//go:build windows
// Package collector 实现 Windows 系统静态信息采集。 // Logan.Li
package collector
import (
"os/exec"
"strings"
)
type SystemInfo struct {
OSVersion string
Kernel string
UptimeSeconds int64
}
func CollectSystem() SystemInfo {
return SystemInfo{
OSVersion: windowsOSCaption(),
Kernel: windowsVersion(),
UptimeSeconds: windowsUptime(),
}
}
func windowsOSCaption() string {
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", "(Get-CimInstance Win32_OperatingSystem).Caption").Output()
if err != nil {
return "Windows"
}
if s := strings.TrimSpace(string(out)); s != "" {
return s
}
return "Windows"
}
func windowsVersion() string {
out, err := exec.Command("cmd.exe", "/C", "ver").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
func windowsUptime() int64 {
out, err := exec.Command("powershell.exe", "-NoProfile", "-Command", "[int]((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalSeconds").Output()
if err != nil {
return 0
}
var n int64
for _, ch := range strings.TrimSpace(string(out)) {
if ch >= '0' && ch <= '9' {
n = n*10 + int64(ch-'0')
}
}
return n
}
+8
View File
@@ -0,0 +1,8 @@
//go:build windows
package collector
func bytesToGB(b uint64) float64 {
const gb = 1024.0 * 1024.0 * 1024.0
return float64(b) / gb
}