feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议 - 增强 RDP 剪贴板、虚拟盘和文件传输能力 - 新增 AIOps 上下文工具、告警 RCA trace 落库和回放接口 - 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
This commit is contained in:
1 parent
a9513c2056
commit
e0d652d211
169 files changed
+12192
-9116
No files matched your search
+8
-3
@@ -10,14 +10,14 @@ LDFLAGS := -s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME)
|
||||
# 静态编译:关闭 CGO 并使用静态链接
|
||||
LINUX_LDFLAGS := -extldflags "-static"
|
||||
|
||||
.PHONY: build cross build-linux-amd64 build-linux-arm64 build-darwin-arm64 tidy run fmt vet clean help
|
||||
.PHONY: build cross build-linux-amd64 build-linux-arm64 build-darwin-arm64 build-windows-amd64 tidy run fmt vet clean help
|
||||
|
||||
## build: 本地构建当前平台二进制
|
||||
build:
|
||||
go build -ldflags "$(LDFLAGS)" -o $(APP) .
|
||||
|
||||
## cross: 交叉编译 Linux amd64/arm64 + macOS arm64
|
||||
cross: build-linux-amd64 build-linux-arm64 build-darwin-arm64
|
||||
## cross: 交叉编译 Linux amd64/arm64 + macOS arm64 + Windows amd64 // Logan.Li
|
||||
cross: build-linux-amd64 build-linux-arm64 build-darwin-arm64 build-windows-amd64
|
||||
|
||||
## build-linux-amd64: 交叉编译 linux/amd64
|
||||
build-linux-amd64:
|
||||
@@ -34,6 +34,11 @@ build-darwin-arm64:
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/$(APP)-darwin-arm64 .
|
||||
|
||||
## build-windows-amd64: 编译 Windows/amd64 Agent // Logan.Li
|
||||
build-windows-amd64:
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/$(APP)-windows-amd64.exe .
|
||||
|
||||
## tidy: 整理依赖
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
+17
-1
@@ -141,6 +141,22 @@ curl -o /usr/local/bin/aiops-agent http://10.10.10.75:8080/agent/aiops-agent-lin
|
||||
curl -fsSL http://10.10.10.75:8080/agent/install.sh | sudo bash -s -- --server http://10.10.10.75:8080 --token <TOKEN>
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Windows Agent 使用同一套 `/api/v1/agent/report` 上报协议,主机监控图表不需要单独分叉。第一版已支持 CPU、内存、逻辑磁盘、监听端口、TCP 连接数、进程 TOP、系统信息、健康探针和 PowerShell 任务执行;Linux 专属的 Load、IOWait、inode、FD、磁盘 IO 等字段在 Windows 上会保持 0 或空值。// Logan.Li
|
||||
|
||||
在管理员 PowerShell 中执行:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -Command "iwr http://10.10.10.75:8080/agent/install.ps1 -UseBasicParsing -OutFile $env:TEMP\install-aiops-agent.ps1; & $env:TEMP\install-aiops-agent.ps1 -Server 'http://10.10.10.75:8080' -Token '<TOKEN>'"
|
||||
```
|
||||
|
||||
手动下载:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri http://10.10.10.75:8080/agent/aiops-agent-windows-amd64.exe -OutFile C:\ProgramData\AIOps\aiops-agent.exe -UseBasicParsing
|
||||
```
|
||||
|
||||
## 本地构建
|
||||
|
||||
```bash
|
||||
@@ -148,4 +164,4 @@ cd agent
|
||||
make # CGO_ENABLED=0 静态编译当前平台
|
||||
```
|
||||
|
||||
Docker 多架构构建由顶层 `backend/Dockerfile` 的 `agent-builder` stage 完成(linux-amd64 / linux-arm64 / darwin-arm64)。
|
||||
顶层 `./run.sh build agent` 会交叉编译 `linux-amd64`、`linux-arm64`、`darwin-arm64`、`windows-amd64.exe`。Docker 镜像会携带当前目标架构 Linux Agent 和 Windows AMD64 Agent。// Logan.Li
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector 的上下文切换采集器。
|
||||
// 读取 /proc/stat 中的 ctxt(上下文切换)和 intr(中断)累计值。
|
||||
// 注意:此处返回累计值而非速率,后端可用两次上报差值计算。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector 提供服务器各项系统指标的采集能力。
|
||||
// 本文件实现 CPU 使用率与负载采集,主要通过解析 Linux /proc/stat 与 /proc/loadavg 实现。
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector 提供服务器各项系统指标的采集能力。
|
||||
// 本文件实现对所有已挂载的真实文件系统执行 statfs,采集磁盘使用情况。
|
||||
// 使用 golang.org/x/sys/unix 的 Statfs,兼容 Linux 与 macOS。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector — 磁盘 IO 指标采集器
|
||||
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
|
||||
//
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector 的 FD 句柄采集器。
|
||||
// 读取 /proc/sys/fs/file-nr 和 /proc/sys/fs/file-max 获取系统级文件句柄使用情况。
|
||||
//
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector 提供服务器各项系统指标的采集能力。
|
||||
// 本文件实现内存使用率采集,通过解析 Linux /proc/meminfo 获取。
|
||||
//
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 网络指标采集器
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector — 进程 TOP 采集器
|
||||
// 扫描 /proc 获取 TOP N 进程完整信息:PID/User/CMD/CPU%/MEM%/FDs
|
||||
// 阿里云 ECS 监控风格 — 直观 6 列布局
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector — Swap 采集器
|
||||
// 从 /proc/meminfo 读取 SwapTotal / SwapFree
|
||||
// 仅当系统有 swap 分区时返回有效数据
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package collector 实现 AIOps Agent 系统静态信息采集。
|
||||
// 仅在 Agent 启动时执行一次,采集 OS 版本、内核版本等信息。
|
||||
//
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Server,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Token = "none",
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$Interval = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Assert-Admin {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
throw "请使用管理员 PowerShell 执行安装脚本"
|
||||
}
|
||||
}
|
||||
|
||||
function Normalize-Server([string]$Value) {
|
||||
return $Value.TrimEnd("/")
|
||||
}
|
||||
|
||||
Assert-Admin
|
||||
$Server = Normalize-Server $Server
|
||||
$InstallDir = "C:\Program Files\AIOps"
|
||||
$LogDir = "C:\ProgramData\AIOps\logs"
|
||||
$AgentPath = Join-Path $InstallDir "aiops-agent.exe"
|
||||
$ServiceName = "AIOpsAgent"
|
||||
$BinaryUrl = "$Server/agent/aiops-agent-windows-amd64.exe"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "AIOps Agent Windows 安装"
|
||||
Write-Host "Server: $Server"
|
||||
Write-Host "Binary: $BinaryUrl"
|
||||
Write-Host ""
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
|
||||
$Tmp = Join-Path $env:TEMP "aiops-agent-windows-amd64.exe"
|
||||
Invoke-WebRequest -Uri $BinaryUrl -OutFile $Tmp -UseBasicParsing
|
||||
Copy-Item -Force $Tmp $AgentPath
|
||||
|
||||
$Args = "-server `"$Server`" -interval $Interval -token `"$Token`""
|
||||
$Existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||
if ($Existing) {
|
||||
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
|
||||
sc.exe delete $ServiceName | Out-Null
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
New-Service -Name $ServiceName `
|
||||
-DisplayName "AIOps Agent" `
|
||||
-Description "AIOps Windows monitoring and task agent" `
|
||||
-BinaryPathName "`"$AgentPath`" $Args" `
|
||||
-StartupType Automatic | Out-Null
|
||||
|
||||
Start-Service -Name $ServiceName
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$Status = (Get-Service -Name $ServiceName).Status
|
||||
if ($Status -ne "Running") {
|
||||
throw "服务启动失败,当前状态: $Status"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "安装成功,AIOps Agent 已作为 Windows 服务启动"
|
||||
Write-Host "查看状态: Get-Service AIOpsAgent"
|
||||
Write-Host "停止服务: Stop-Service AIOpsAgent"
|
||||
Write-Host "卸载服务: Stop-Service AIOpsAgent; sc.exe delete AIOpsAgent"
|
||||
@@ -63,8 +63,8 @@ func (c *Client) PollProbes(ctx context.Context) ([]model.ProbeConfig, error) {
|
||||
path := probePath + "?hostname=" + url.QueryEscape(c.hostname)
|
||||
|
||||
var wrapper struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []model.ProbeConfig `json:"data"`
|
||||
}
|
||||
if err := c.client.DoJSON(ctx, "GET", path, nil, &wrapper); err != nil {
|
||||
|
||||
+2
-22
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
// Package task 实现 AIOps Agent 的任务拉取与执行能力。
|
||||
// Agent 通过轮询接口从后端获取待执行任务(命令/脚本),本地执行后回传结果。
|
||||
//
|
||||
@@ -13,28 +15,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 任务类型常量
|
||||
const (
|
||||
TypeCommand = "command" // 单条 shell 命令
|
||||
TypeScript = "script" // 多行 bash 脚本
|
||||
)
|
||||
|
||||
// Task 表示从后端拉取到的待执行任务。
|
||||
type Task struct {
|
||||
TaskID string `json:"task_id"` // 任务唯一标识,回传结果时使用
|
||||
Type string `json:"type"` // 任务类型:command / script
|
||||
Command string `json:"command"` // 命令内容(command 时为单条命令,script 时为脚本正文)
|
||||
Timeout int `json:"timeout"` // 执行超时,单位:秒;<=0 表示不限制
|
||||
}
|
||||
|
||||
// Result 表示任务执行结果,用于回传后端。
|
||||
type Result struct {
|
||||
TaskID string `json:"task_id"` // 对应的任务 ID
|
||||
ExitCode int `json:"exit_code"` // 进程退出码;超时/启动失败时为 -1
|
||||
Result string `json:"result"` // 合并后的 stdout+stderr 输出
|
||||
Status string `json:"status"` // 执行状态:success / failed / timeout
|
||||
}
|
||||
|
||||
// Executor 负责本地执行任务并产出结果。
|
||||
type Executor struct {
|
||||
logger *log.Logger
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//go:build windows
|
||||
|
||||
// Package task 实现 Windows Agent 的 PowerShell 任务执行能力。 // Logan.Li
|
||||
package task
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Executor struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func New(logger *log.Logger) *Executor {
|
||||
return &Executor{logger: logger}
|
||||
}
|
||||
|
||||
func (e *Executor) ExecuteTask(ctx context.Context, t Task) Result {
|
||||
res := Result{TaskID: t.TaskID}
|
||||
execCtx, cancel := e.withTimeout(ctx, t.Timeout)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var cleanup func()
|
||||
switch t.Type {
|
||||
case TypeCommand:
|
||||
cmd = exec.CommandContext(execCtx, "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", t.Command)
|
||||
case TypeScript:
|
||||
path, err := writeTempPowerShell(t.Command)
|
||||
if err != nil {
|
||||
res.ExitCode = -1
|
||||
res.Status = "failed"
|
||||
res.Result = "写入临时 PowerShell 脚本失败: " + err.Error()
|
||||
return res
|
||||
}
|
||||
cleanup = func() { _ = os.Remove(path) }
|
||||
cmd = exec.CommandContext(execCtx, "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path)
|
||||
default:
|
||||
res.ExitCode = -1
|
||||
res.Status = "failed"
|
||||
res.Result = fmt.Sprintf("未知的任务类型: %q", t.Type)
|
||||
return res
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
start := time.Now()
|
||||
err := cmd.Run()
|
||||
elapsed := time.Since(start)
|
||||
if execCtx.Err() == context.DeadlineExceeded {
|
||||
res.ExitCode = -1
|
||||
res.Status = "timeout"
|
||||
res.Result = fmt.Sprintf("任务执行超时(%ds),已输出:\n%s", t.Timeout, buf.String())
|
||||
e.logger.Printf("任务 %s 执行超时(%ds)", t.TaskID, t.Timeout)
|
||||
return res
|
||||
}
|
||||
res.ExitCode = exitCode(err)
|
||||
res.Result = buf.String()
|
||||
if res.ExitCode == 0 {
|
||||
res.Status = "success"
|
||||
e.logger.Printf("任务 %s 执行成功 exit=0 耗时=%s 输出=%dB", t.TaskID, elapsed, buf.Len())
|
||||
} else {
|
||||
res.Status = "failed"
|
||||
e.logger.Printf("任务 %s 执行失败 exit=%d 耗时=%s 输出=%dB", t.TaskID, res.ExitCode, elapsed, buf.Len())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (e *Executor) withTimeout(parent context.Context, timeout int) (context.Context, context.CancelFunc) {
|
||||
if timeout <= 0 {
|
||||
return parent, func() {}
|
||||
}
|
||||
return context.WithTimeout(parent, time.Duration(timeout)*time.Second)
|
||||
}
|
||||
|
||||
func exitCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func writeTempPowerShell(script string) (string, error) {
|
||||
path := filepath.Join(os.TempDir(), fmt.Sprintf("aiops-agent-task-%d.ps1", time.Now().UnixNano()))
|
||||
if err := os.WriteFile(path, []byte(script), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Package task 定义 Agent 远程任务类型。 // Logan.Li
|
||||
package task
|
||||
|
||||
const (
|
||||
TypeCommand = "command" // 单条 shell/PowerShell 命令 // Logan.Li
|
||||
TypeScript = "script" // 多行脚本 // Logan.Li
|
||||
)
|
||||
|
||||
// Task 表示从后端拉取到的待执行任务。 // Logan.Li
|
||||
type Task struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Type string `json:"type"`
|
||||
Command string `json:"command"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
// Result 表示任务执行结果,用于回传后端。 // Logan.Li
|
||||
type Result struct {
|
||||
TaskID string `json:"task_id"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Result string `json:"result"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
Reference in new issue
Block a user