Files
aiops/backend/pkg/aiopscontext/service.go
T
Logan.Li e0d652d211 feat: 完善堡垒机与 Agent 智能运维能力
- 支持 Windows Agent 构建、安装脚本和统一上报协议

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

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

- 重构 docs 模块文档并补充终端、堡垒机、监控、AI Agent 设计
2026-07-05 23:49:36 +08:00

434 lines
12 KiB
Go

package aiopscontext
import (
"context"
"fmt"
"math"
"sort"
"strings"
"time"
"ezops/model"
"gorm.io/gorm"
)
// Service 汇聚 AIOps 平台上下文,后续可复用为 MCP Server 的数据源。 // Logan.Li
type Service struct {
db *gorm.DB
}
// NewService 创建上下文服务。 // Logan.Li
func NewService(db *gorm.DB) *Service {
return &Service{db: db}
}
// Summary 返回平台总览上下文。 // Logan.Li
func (s *Service) Summary(ctx context.Context) (*Summary, error) {
out := Summary{
HighLoadHosts: []AssetSummary{},
RecentAlerts: []AlertSummary{},
}
db := s.db.WithContext(ctx)
db.Model(&model.Server{}).Count(&out.ServerTotal)
db.Model(&model.Server{}).Where("agent_status = ? OR status = ?", "online", "running").Count(&out.ServerOnline)
db.Model(&model.KubernetesCluster{}).Count(&out.ClusterTotal)
db.Model(&model.AlertEvent{}).Where("status = ?", "firing").Count(&out.FiringAlerts)
db.Model(&model.AgentAlert{}).Where("resolved = ?", false).Count(&out.UnresolvedAlerts)
assets, err := s.SearchAssets(ctx, "", 8)
if err == nil {
for _, a := range assets {
if a.CPUUsage >= 80 || a.MemoryUsage >= 80 || a.DiskUsage >= 85 {
out.HighLoadHosts = append(out.HighLoadHosts, a)
}
}
}
out.RecentAlerts, _ = s.ListAlerts(ctx, AlertQuery{Limit: 10})
out.GeneratedAt = time.Now()
return &out, nil
}
// SearchAssets 查询资产摘要,默认按更新时间倒序返回。 // Logan.Li
func (s *Service) SearchAssets(ctx context.Context, keyword string, limit int) ([]AssetSummary, error) {
if limit <= 0 || limit > 50 {
limit = 10
}
var servers []model.Server
q := s.db.WithContext(ctx).Model(&model.Server{})
kw := strings.TrimSpace(keyword)
if kw != "" {
like := "%" + kw + "%"
q = q.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ? OR tags LIKE ? OR description LIKE ?",
like, like, like, like, like)
}
if err := q.Order("updated_at DESC").Limit(limit).Find(&servers).Error; err != nil {
return nil, err
}
out := make([]AssetSummary, 0, len(servers))
for _, srv := range servers {
out = append(out, assetFromServer(srv))
}
return out, nil
}
// ListAlerts 查询统一告警摘要。 // Logan.Li
func (s *Service) ListAlerts(ctx context.Context, query AlertQuery) ([]AlertSummary, error) {
limit := query.Limit
if limit <= 0 || limit > 100 {
limit = 20
}
since := sinceByRange(query.TimeRange, 24*time.Hour)
targetLike := "%" + strings.TrimSpace(query.Target) + "%"
var events []model.AlertEvent
q := s.db.WithContext(ctx).Model(&model.AlertEvent{}).Where("created_at >= ?", since)
if query.Target != "" {
q = q.Where("target LIKE ? OR target_name LIKE ? OR target_ip LIKE ? OR rule_name LIKE ?",
targetLike, targetLike, targetLike, targetLike)
}
if query.Severity != "" {
q = q.Where("severity = ?", query.Severity)
}
if query.Status != "" {
q = q.Where("status = ?", query.Status)
}
if err := q.Order("created_at DESC").Limit(limit).Find(&events).Error; err != nil {
return nil, err
}
out := make([]AlertSummary, 0, len(events)+limit)
for _, e := range events {
out = append(out, alertFromEvent(e))
}
var agentAlerts []model.AgentAlert
aq := s.db.WithContext(ctx).Model(&model.AgentAlert{}).Where("created_at >= ?", since)
if query.Target != "" {
aq = aq.Where("hostname LIKE ? OR rule_name LIKE ?", targetLike, targetLike)
}
if query.Severity != "" {
aq = aq.Where("severity = ?", query.Severity)
}
if query.Status == "firing" || query.Status == "active" {
aq = aq.Where("resolved = ?", false)
} else if query.Status == "resolved" {
aq = aq.Where("resolved = ?", true)
}
if err := aq.Order("created_at DESC").Limit(limit).Find(&agentAlerts).Error; err == nil {
for _, a := range agentAlerts {
out = append(out, alertFromAgent(a))
}
}
sort.SliceStable(out, func(i, j int) bool {
return out[i].StartedAt.After(out[j].StartedAt)
})
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
// MetricSummary 查询主机指标摘要。 // Logan.Li
func (s *Service) MetricSummary(ctx context.Context, target, metric, timeRange string, limit int) (*MetricSummary, error) {
hostname, err := s.resolveHostname(ctx, target)
if err != nil {
return nil, err
}
if metric == "" {
metric = "cpu_usage"
}
if limit <= 0 || limit > 500 {
limit = 240
}
since := sinceByRange(timeRange, time.Hour)
var reports []model.AgentReport
if err := s.db.WithContext(ctx).Where("hostname = ? AND report_time >= ?", hostname, since).
Order("report_time ASC").Limit(limit).Find(&reports).Error; err != nil {
return nil, err
}
out := &MetricSummary{Target: target, Hostname: hostname, Metric: metric, TimeRange: normalizeRangeLabel(timeRange), Points: []MetricPoint{}}
values := make([]float64, 0, len(reports))
for _, r := range reports {
v, ok := metricValue(r, metric)
if !ok {
continue
}
values = append(values, v)
if len(out.Points) < 60 || len(reports) <= 60 {
out.Points = append(out.Points, MetricPoint{Time: r.ReportTime, Value: round2(v)})
}
}
fillMetricStats(out, values)
return out, nil
}
// BuildPack 构建面向 LLM 的上下文包。 // Logan.Li
func (s *Service) BuildPack(ctx context.Context, req ContextPackRequest) (*ContextPack, error) {
if req.Limit <= 0 {
req.Limit = 10
}
scope := strings.TrimSpace(req.Target)
var primaryAlert *AlertSummary
if req.AlertID > 0 {
alert, err := s.GetAlert(ctx, req.AlertID)
if err == nil {
primaryAlert = alert
if scope == "" {
scope = alert.Target
}
}
}
pack := &ContextPack{
Scope: scope, TimeRange: normalizeRangeLabel(req.TimeRange),
Assets: []AssetSummary{}, Alerts: []AlertSummary{}, Metrics: []MetricSummary{},
GeneratedAt: time.Now(), Evidence: []EvidenceItem{},
}
if scope != "" {
pack.Assets, _ = s.SearchAssets(ctx, scope, req.Limit)
for _, asset := range pack.Assets {
pack.Evidence = append(pack.Evidence, EvidenceItem{
Type: "asset", Source: "asset_servers",
Summary: fmt.Sprintf("资产 %s ip=%s protocol=%s os=%s status=%s agent=%s",
asset.Hostname, asset.IP, asset.Protocol, asset.OSType, asset.Status, asset.AgentStatus),
Confidence: 0.9,
})
}
}
pack.Alerts, _ = s.ListAlerts(ctx, AlertQuery{Target: scope, TimeRange: req.TimeRange, Limit: req.Limit})
if primaryAlert != nil {
pack.Alerts = prependAlertIfMissing(pack.Alerts, *primaryAlert)
}
metrics := req.Metrics
if len(metrics) == 0 {
metrics = []string{"cpu_usage", "mem_usage", "load1"}
}
for _, metric := range metrics {
if scope == "" {
break
}
ms, err := s.MetricSummary(ctx, scope, metric, req.TimeRange, 240)
if err == nil && ms.SampleSize > 0 {
pack.Metrics = append(pack.Metrics, *ms)
pack.Evidence = append(pack.Evidence, EvidenceItem{
Type: "metric", Source: "monitor_agent_reports",
Summary: fmt.Sprintf("%s 最近 %s: latest=%.2f avg=%.2f p95=%.2f max=%.2f trend=%s",
ms.Metric, ms.TimeRange, ms.Latest, ms.Avg, ms.P95, ms.Max, ms.Trend),
Confidence: 0.82,
})
}
}
for _, a := range pack.Alerts {
pack.Evidence = append(pack.Evidence, EvidenceItem{
Type: "alert", Source: a.Source,
Summary: fmt.Sprintf("[%s|%s] %s target=%s value=%.2f threshold=%.2f",
a.Severity, a.Status, a.RuleName, a.Target, a.Value, a.Threshold),
Confidence: 0.78,
})
}
return pack, nil
}
// GetAlert 获取单条平台告警摘要。 // Logan.Li
func (s *Service) GetAlert(ctx context.Context, id uint) (*AlertSummary, error) {
var event model.AlertEvent
if err := s.db.WithContext(ctx).Take(&event, id).Error; err == nil {
alert := alertFromEvent(event)
return &alert, nil
}
var agentAlert model.AgentAlert
if err := s.db.WithContext(ctx).Take(&agentAlert, id).Error; err != nil {
return nil, err
}
alert := alertFromAgent(agentAlert)
return &alert, nil
}
func (s *Service) resolveHostname(ctx context.Context, target string) (string, error) {
target = strings.TrimSpace(target)
if target == "" {
return "", fmt.Errorf("target 不能为空")
}
var srv model.Server
err := s.db.WithContext(ctx).Where("hostname = ? OR ip = ? OR public_ip = ?", target, target, target).Take(&srv).Error
if err == nil {
return srv.Hostname, nil
}
if err == gorm.ErrRecordNotFound {
return target, nil
}
return "", err
}
func assetFromServer(srv model.Server) AssetSummary {
return AssetSummary{
ID: srv.ID, Hostname: srv.Hostname, IP: srv.IP, PublicIP: srv.PublicIP,
Protocol: srv.Protocol, OSType: srv.OSType, OS: srv.OS, Status: srv.Status,
AgentStatus: srv.AgentStatus, CPUUsage: srv.CPUUsage, MemoryUsage: srv.MemoryUsage,
DiskUsage: srv.DiskUsage, Tags: srv.Tags, Description: srv.Description,
LastCheckAt: srv.LastCheckAt, UpdatedAt: srv.UpdatedAt,
}
}
func alertFromEvent(e model.AlertEvent) AlertSummary {
target := e.Target
if target == "" {
target = e.TargetName
}
if target == "" {
target = e.TargetIP
}
started := e.StartedAt
if started.IsZero() {
started = e.CreatedAt
}
return AlertSummary{
ID: e.ID, Source: "monitor_alert_events", RuleName: firstNonEmpty(e.RuleName, e.Title),
Severity: e.Severity, Status: e.Status, Target: target, Metric: e.MetricName,
Value: e.Value, Threshold: e.Threshold, Message: e.Message, StartedAt: started,
ResolvedAt: e.ResolvedAt,
}
}
func alertFromAgent(a model.AgentAlert) AlertSummary {
status := "firing"
if a.Resolved {
status = "resolved"
}
return AlertSummary{
ID: a.ID, Source: "monitor_agent_alerts", RuleName: a.RuleName,
Severity: a.Severity, Status: status, Target: a.Hostname, Metric: a.Metric,
Value: a.Value, Threshold: a.Threshold, Message: a.Message,
StartedAt: a.CreatedAt, ResolvedAt: a.ResolvedAt,
}
}
func prependAlertIfMissing(alerts []AlertSummary, alert AlertSummary) []AlertSummary {
for _, item := range alerts {
if item.Source == alert.Source && item.ID == alert.ID {
return alerts
}
}
return append([]AlertSummary{alert}, alerts...)
}
func metricValue(r model.AgentReport, metric string) (float64, bool) {
switch metric {
case "cpu", "cpu_usage":
return r.CPUUsage, true
case "cpu_iowait":
return r.CPUIOwait, true
case "mem", "memory", "mem_usage", "memory_usage":
return r.MemUsage, true
case "mem_available":
return r.MemAvailable, true
case "swap", "mem_swap_used":
return r.MemSwapUsed, true
case "load1":
return r.Load1, true
case "load5":
return r.Load5, true
case "load15":
return r.Load15, true
default:
return 0, false
}
}
func fillMetricStats(out *MetricSummary, values []float64) {
out.SampleSize = len(values)
if len(values) == 0 {
out.Trend = "unknown"
return
}
minV, maxV, sum := values[0], values[0], 0.0
for _, v := range values {
if v < minV {
minV = v
}
if v > maxV {
maxV = v
}
sum += v
}
sorted := append([]float64(nil), values...)
sort.Float64s(sorted)
p95Idx := int(math.Ceil(float64(len(sorted))*0.95)) - 1
if p95Idx < 0 {
p95Idx = 0
}
out.Latest = round2(values[len(values)-1])
out.Min = round2(minV)
out.Max = round2(maxV)
out.Avg = round2(sum / float64(len(values)))
out.P95 = round2(sorted[p95Idx])
out.Trend = trendOf(values)
}
func trendOf(values []float64) string {
if len(values) < 4 {
return "stable"
}
n := len(values)
head := avg(values[:n/3])
tail := avg(values[n-n/3:])
if tail-head > 5 {
return "up"
}
if head-tail > 5 {
return "down"
}
return "stable"
}
func avg(values []float64) float64 {
if len(values) == 0 {
return 0
}
sum := 0.0
for _, v := range values {
sum += v
}
return sum / float64(len(values))
}
func sinceByRange(label string, fallback time.Duration) time.Time {
switch strings.ToLower(strings.TrimSpace(label)) {
case "15m":
return time.Now().Add(-15 * time.Minute)
case "30m":
return time.Now().Add(-30 * time.Minute)
case "2h":
return time.Now().Add(-2 * time.Hour)
case "6h":
return time.Now().Add(-6 * time.Hour)
case "24h", "1d":
return time.Now().Add(-24 * time.Hour)
case "7d":
return time.Now().Add(-7 * 24 * time.Hour)
default:
return time.Now().Add(-fallback)
}
}
func normalizeRangeLabel(label string) string {
if strings.TrimSpace(label) == "" {
return "1h"
}
return label
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
func round2(v float64) float64 {
return math.Round(v*100) / 100
}