package main import ( "fmt" "sort" "strings" ) type ServiceLink struct { ServiceID int64 `json:"serviceId"` AgentID string `json:"agentId"` VMID int `json:"vmid"` ComponentType string `json:"componentType"` ComponentName string `json:"componentName"` Port int `json:"port"` } func (s *Store) ServiceLinks() ([]ServiceLink, error) { rows, err := s.db.Query(`SELECT service_id,agent_id,vmid,component_type,component_name,port FROM service_links ORDER BY service_id`) if err != nil { return nil, err } defer rows.Close() var out []ServiceLink for rows.Next() { var v ServiceLink if err = rows.Scan(&v.ServiceID, &v.AgentID, &v.VMID, &v.ComponentType, &v.ComponentName, &v.Port); err != nil { return nil, err } out = append(out, v) } return out, rows.Err() } func (s *Store) SaveServiceLink(v ServiceLink) (ServiceLink, error) { v.AgentID = strings.TrimSpace(v.AgentID) v.ComponentType = strings.TrimSpace(v.ComponentType) v.ComponentName = strings.TrimSpace(v.ComponentName) if v.ServiceID <= 0 { return v, fmt.Errorf("выберите сервис") } if v.AgentID == "" && v.VMID <= 0 { return v, fmt.Errorf("выберите агент или VMID") } if v.ComponentType != "" && v.ComponentType != "systemd" && v.ComponentType != "docker" && v.ComponentType != "process" { return v, fmt.Errorf("неизвестный тип компонента") } if v.Port < 0 || v.Port > 65535 { return v, fmt.Errorf("некорректный порт") } _, err := s.db.Exec(`INSERT INTO service_links(service_id,agent_id,vmid,component_type,component_name,port) VALUES(?,?,?,?,?,?) ON CONFLICT(service_id) DO UPDATE SET agent_id=excluded.agent_id,vmid=excluded.vmid,component_type=excluded.component_type,component_name=excluded.component_name,port=excluded.port`, v.ServiceID, v.AgentID, v.VMID, v.ComponentType, v.ComponentName, v.Port) return v, err } func (s *Store) DeleteServiceLink(serviceID int64) error { _, err := s.db.Exec(`DELETE FROM service_links WHERE service_id=?`, serviceID) return err } func groupLinkedAlerts(metrics dashboardMetrics, alerts []Alert) []Alert { if len(metrics.ServiceLinks) == 0 { return alerts } agentOffline := map[string]ManagedAgent{} for _, agent := range metrics.Agents { if !agent.Online { agentOffline[agent.ID] = agent } } affected := map[string][]string{} remove := map[string]bool{} for _, link := range metrics.ServiceLinks { agent, offline := agentOffline[link.AgentID] if !offline { continue } for _, service := range metrics.Services.Services { if service.ID != link.ServiceID { continue } for _, endpoint := range service.Endpoints { if !endpoint.Up { key := fmt.Sprintf("service-%d-%s", service.ID, endpoint.Kind) remove[key] = true affected[agent.ID] = append(affected[agent.ID], service.Name) } } } } if len(affected) == 0 { return alerts } out := make([]Alert, 0, len(alerts)) for _, alert := range alerts { skip := remove[alert.ID] for agentID := range affected { if alert.ID == "agent-offline-"+agentID+fmt.Sprintf("-%d", agentOffline[agentID].VMID) { skip = true } } if !skip { out = append(out, alert) } } for agentID, names := range affected { sort.Strings(names) agent := agentOffline[agentID] out = append(out, Alert{ID: "group-agent-offline-" + agentID + fmt.Sprintf("-%d", agent.VMID), Severity: "critical", Source: "Корреляция", Title: agent.Name + ": агент недоступен, связанные сервисы не отвечают", Message: fmt.Sprintf("VMID %d. Затронуто сервисов: %d — %s. Вероятная первопричина: недоступна гостевая система или агент.", agent.VMID, len(names), strings.Join(names, ", "))}) } return out }