Files
aiops/agent/collector/health_probe.go
T
attacker d656520428 feat: v0.34 健康探针(Architecture B) + Agent v2.1 + 端口监控 + 告警联系人重构
- v0.31: AlertContact 重构(channel_type下拉+webhook_url替代三分立字段)
- v0.32: Agent ProcessTop 增强(cmdline/user/cpu_pct/mem_pct)
- v0.33: 端口监控 Tab(监听端口自动发现+down联动告警)
- v0.34: 健康探针(Agent本地执行HTTP/TCP/Exec→结果随心跳上报)
- Agent新增: ContextSwitch/FD/HealthProbe采集器
- 后端新增: health_probe handler + Agent model扩展
- HostMonitor新增Tab: 健康探针/端口监控
2026-06-24 19:18:10 +08:00

217 lines
5.5 KiB
Go

// Package collector 的健康探针采集器。
// 在后端下发的探针配置驱动下,本地执行 HTTP GET / TCP 连接 / 命令执行,
// 并将结果返回供上报。
//
// 署名 Logan.Li
package collector
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os/exec"
"strings"
"time"
"ezops/agent/model"
)
// HealthProbeCollector 健康探针执行器
type HealthProbeCollector struct {
httpClient *http.Client
}
// NewHealthProbeCollector 创建采集器实例
func NewHealthProbeCollector() *HealthProbeCollector {
return &HealthProbeCollector{
httpClient: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
}).DialContext,
},
},
}
}
// Execute 执行所有探针配置,返回结果列表。
// parentCtx 用于传播取消信号——探针执行尽量尊重主循环的生命周期。
func (h *HealthProbeCollector) Execute(parentCtx context.Context, configs []model.ProbeConfig) []model.ProbeResult {
results := make([]model.ProbeResult, 0, len(configs))
for _, cfg := range configs {
// 检查父上下文是否已取消
if err := parentCtx.Err(); err != nil {
break
}
result := model.ProbeResult{
ProbeID: cfg.ID,
Name: cfg.Name,
ProbeType: cfg.ProbeType,
}
start := time.Now()
switch cfg.ProbeType {
case "http_get":
result = h.execHTTP(parentCtx, cfg, start)
case "tcp_socket":
result = h.execTCP(cfg, start)
case "exec_command":
result = h.execCmd(parentCtx, cfg, start)
default:
result.Status = "down"
result.Message = fmt.Sprintf("unknown probe type: %s", cfg.ProbeType)
}
results = append(results, result)
}
return results
}
// execHTTP 执行 HTTP 探测,接收父 context
func (h *HealthProbeCollector) execHTTP(parentCtx context.Context, cfg model.ProbeConfig, start time.Time) model.ProbeResult {
result := model.ProbeResult{
ProbeID: cfg.ID,
Name: cfg.Name,
ProbeType: cfg.ProbeType,
}
timeout := time.Duration(cfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 10 * time.Second
}
ctx, cancel := context.WithTimeout(parentCtx, timeout)
defer cancel()
method := cfg.HTTPMethod
if method == "" {
method = "GET"
}
req, err := http.NewRequestWithContext(ctx, method, cfg.HTTPURL, nil)
if err != nil {
result.Status = "down"
result.Message = fmt.Sprintf("构造请求失败: %v", err)
result.LatencyMs = int(time.Since(start).Milliseconds())
return result
}
req.Header.Set("User-Agent", "aiops-agent-health-probe/2.1")
resp, err := h.httpClient.Do(req)
latencyMs := int(time.Since(start).Milliseconds())
result.LatencyMs = latencyMs
if err != nil {
result.Status = "down"
result.Message = fmt.Sprintf("HTTP请求失败: %v", err)
return result
}
defer resp.Body.Close()
expectCode := cfg.HTTPExpectCode
if expectCode == 0 {
expectCode = 200
}
result.StatusCode = resp.StatusCode
if resp.StatusCode != expectCode {
result.Status = "down"
result.Message = fmt.Sprintf("状态码不匹配: got %d, want %d", resp.StatusCode, expectCode)
return result
}
// 可选:检查响应体
if cfg.HTTPExpectBody != "" {
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
result.Status = "down"
result.Message = fmt.Sprintf("读取响应体失败: %v", err)
return result
}
if !strings.Contains(string(bodyBytes), cfg.HTTPExpectBody) {
result.Status = "down"
result.Message = fmt.Sprintf("响应体不包含期望字符串: %q", cfg.HTTPExpectBody)
return result
}
}
result.Status = "up"
result.Message = fmt.Sprintf("HTTP %d OK (%dms)", resp.StatusCode, latencyMs)
return result
}
// execTCP 执行 TCP 连接探测
func (h *HealthProbeCollector) execTCP(cfg model.ProbeConfig, start time.Time) model.ProbeResult {
result := model.ProbeResult{
ProbeID: cfg.ID,
Name: cfg.Name,
ProbeType: cfg.ProbeType,
}
timeout := time.Duration(cfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 10 * time.Second
}
addr := fmt.Sprintf("%s:%d", cfg.TCPHost, cfg.TCPPort)
conn, err := net.DialTimeout("tcp", addr, timeout)
latencyMs := int(time.Since(start).Milliseconds())
result.LatencyMs = latencyMs
if err != nil {
result.Status = "down"
result.Message = fmt.Sprintf("TCP连接失败 %s: %v", addr, err)
return result
}
conn.Close()
result.Status = "up"
result.Message = fmt.Sprintf("TCP %s connected (%dms)", addr, latencyMs)
return result
}
// execCmd 执行命令探测(exit code 0 = up),接收父 context
func (h *HealthProbeCollector) execCmd(parentCtx context.Context, cfg model.ProbeConfig, start time.Time) model.ProbeResult {
result := model.ProbeResult{
ProbeID: cfg.ID,
Name: cfg.Name,
ProbeType: cfg.ProbeType,
}
timeout := time.Duration(cfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 10 * time.Second
}
ctx, cancel := context.WithTimeout(parentCtx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", cfg.ExecCommand)
output, err := cmd.CombinedOutput()
latencyMs := int(time.Since(start).Milliseconds())
result.LatencyMs = latencyMs
result.StatusCode = cmd.ProcessState.ExitCode()
if err != nil {
result.Status = "down"
// 截断输出避免过大
outMsg := strings.TrimSpace(string(output))
if len(outMsg) > 200 {
outMsg = outMsg[:200] + "..."
}
result.Message = fmt.Sprintf("命令执行失败(exit=%d): %s", cmd.ProcessState.ExitCode(), outMsg)
return result
}
result.Status = "up"
result.Message = fmt.Sprintf("命令执行成功 (%dms)", latencyMs)
return result
}