diff --git a/internal/models/property.go b/internal/models/property.go index 2ef57ef..15284d3 100644 --- a/internal/models/property.go +++ b/internal/models/property.go @@ -65,6 +65,7 @@ type SystemConfig struct { // AlertConfig 全局告警配置 type AlertConfig struct { Enabled bool `json:"enabled"` // 是否启用全局告警 + MaskIP bool `json:"maskIP"` // 是否在通知中打码 IP 地址 Rules AlertRules `json:"rules"` // 告警规则 } diff --git a/internal/service/alert_service.go b/internal/service/alert_service.go index 9c91f4c..d8440d6 100644 --- a/internal/service/alert_service.go +++ b/internal/service/alert_service.go @@ -308,6 +308,13 @@ func (s *AlertService) sendAlertNotification(record *models.AlertRecord, agent * ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // 获取告警配置(包含 MaskIP 设置) + alertConfig, err := s.propertyService.GetAlertConfig(ctx) + if err != nil { + s.logger.Error("获取告警配置失败", zap.Error(err)) + return + } + channelConfigs, err := s.propertyService.GetNotificationChannelConfigs(ctx) if err != nil { s.logger.Error("获取通知渠道配置失败", zap.Error(err)) @@ -325,7 +332,7 @@ func (s *AlertService) sendAlertNotification(record *models.AlertRecord, agent * return } - if err := s.notifier.SendNotificationByConfigs(ctx, enabledChannels, record, agent); err != nil { + if err := s.notifier.SendNotificationByConfigs(ctx, enabledChannels, record, agent, alertConfig.MaskIP); err != nil { s.logger.Error("发送告警通知失败", zap.Error(err)) } } diff --git a/internal/service/notifier.go b/internal/service/notifier.go index dbdc502..2cf422e 100644 --- a/internal/service/notifier.go +++ b/internal/service/notifier.go @@ -32,8 +32,22 @@ func NewNotifier(logger *zap.Logger) *Notifier { } } +// maskIPAddress 打码 IP 地址 (例如: 192.168.1.100 -> 192.168.*.*) +func maskIPAddress(ip string) string { + parts := strings.Split(ip, ".") + if len(parts) == 4 { + // IPv4: 保留前两段,后两段打码 + return parts[0] + "." + parts[1] + ".*.*" + } + // IPv6 或其他格式: 保留前半部分,后半部分打码 + if len(ip) > 8 { + return ip[:len(ip)/2] + "****" + } + return "****" +} + // buildMessage 构建告警消息文本 -func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord) string { +func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord, maskIP bool) string { var message string // 告警级别图标 @@ -76,6 +90,12 @@ func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord) valueUnit = "秒" } + // 处理 IP 地址显示 + displayIP := agent.IP + if maskIP { + displayIP = maskIPAddress(agent.IP) + } + if record.Status == "firing" { // 告警触发消息 message = fmt.Sprintf( @@ -93,7 +113,7 @@ func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord) agent.Name, agent.ID, agent.Hostname, - agent.IP, + displayIP, record.AlertType, record.Message, record.Threshold, @@ -103,6 +123,25 @@ func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord) time.Unix(record.FiredAt/1000, 0).Local().Format("2006-01-02 15:04:05"), ) } else if record.Status == "resolved" { + // 计算持续时间 + durationStr := "" + if record.FiredAt > 0 && record.ResolvedAt > record.FiredAt { + durationMs := record.ResolvedAt - record.FiredAt + durationSec := durationMs / 1000 + if durationSec < 60 { + durationStr = fmt.Sprintf("%d秒", durationSec) + } else if durationSec < 3600 { + minutes := durationSec / 60 + seconds := durationSec % 60 + durationStr = fmt.Sprintf("%d分%d秒", minutes, seconds) + } else { + hours := durationSec / 3600 + minutes := (durationSec % 3600) / 60 + seconds := durationSec % 60 + durationStr = fmt.Sprintf("%d时%d分%d秒", hours, minutes, seconds) + } + } + // 告警恢复消息 message = fmt.Sprintf( "✅ %s已恢复\n\n"+ @@ -111,15 +150,17 @@ func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord) "IP: %s\n"+ "告警类型: %s\n"+ "当前值: %.2f%s\n"+ + "持续时间: %s\n"+ "恢复时间: %s", alertTypeName, agent.Name, agent.ID, agent.Hostname, - agent.IP, + displayIP, record.AlertType, record.ActualValue, valueUnit, + durationStr, time.Unix(record.ResolvedAt/1000, 0).Local().Format("2006-01-02 15:04:05"), ) } @@ -251,7 +292,7 @@ func (n *Notifier) sendEmail(ctx context.Context, smtpHost string, smtpPort int, } // sendCustomWebhook 发送自定义Webhook -func (n *Notifier) sendCustomWebhook(ctx context.Context, config map[string]interface{}, agent *models.Agent, record *models.AlertRecord) error { +func (n *Notifier) sendCustomWebhook(ctx context.Context, config map[string]interface{}, agent *models.Agent, record *models.AlertRecord, maskIP bool) error { // 解析配置 webhookURL, ok := config["url"].(string) if !ok || webhookURL == "" { @@ -281,7 +322,7 @@ func (n *Notifier) sendCustomWebhook(ctx context.Context, config map[string]inte } // 构建消息内容 - message := n.buildMessage(agent, record) + message := n.buildMessage(agent, record, maskIP) // 根据模板类型构建请求体 var reqBody io.Reader @@ -587,12 +628,12 @@ func (n *Notifier) sendEmailByConfig(ctx context.Context, config map[string]inte } // sendWebhookByConfig 根据配置发送自定义Webhook -func (n *Notifier) sendWebhookByConfig(ctx context.Context, config map[string]interface{}, agent *models.Agent, record *models.AlertRecord) error { - return n.sendCustomWebhook(ctx, config, agent, record) +func (n *Notifier) sendWebhookByConfig(ctx context.Context, config map[string]interface{}, agent *models.Agent, record *models.AlertRecord, maskIP bool) error { + return n.sendCustomWebhook(ctx, config, agent, record, maskIP) } // SendNotificationByConfig 根据新的配置结构发送通知 -func (n *Notifier) SendNotificationByConfig(ctx context.Context, channelConfig *models.NotificationChannelConfig, record *models.AlertRecord, agent *models.Agent) error { +func (n *Notifier) SendNotificationByConfig(ctx context.Context, channelConfig *models.NotificationChannelConfig, record *models.AlertRecord, agent *models.Agent, maskIP bool) error { if !channelConfig.Enabled { return fmt.Errorf("通知渠道已禁用") } @@ -602,7 +643,7 @@ func (n *Notifier) SendNotificationByConfig(ctx context.Context, channelConfig * ) // 构造通知消息内容 - message := n.buildMessage(agent, record) + message := n.buildMessage(agent, record, maskIP) switch channelConfig.Type { case "dingtalk": @@ -616,18 +657,18 @@ func (n *Notifier) SendNotificationByConfig(ctx context.Context, channelConfig * case "email": return n.sendEmailByConfig(ctx, channelConfig.Config, message) case "webhook": - return n.sendWebhookByConfig(ctx, channelConfig.Config, agent, record) + return n.sendWebhookByConfig(ctx, channelConfig.Config, agent, record, maskIP) default: return fmt.Errorf("不支持的通知渠道类型: %s", channelConfig.Type) } } // SendNotificationByConfigs 根据新的配置结构向多个渠道发送通知 -func (n *Notifier) SendNotificationByConfigs(ctx context.Context, channelConfigs []models.NotificationChannelConfig, record *models.AlertRecord, agent *models.Agent) error { +func (n *Notifier) SendNotificationByConfigs(ctx context.Context, channelConfigs []models.NotificationChannelConfig, record *models.AlertRecord, agent *models.Agent, maskIP bool) error { var errs []error for _, channelConfig := range channelConfigs { - if err := n.SendNotificationByConfig(ctx, &channelConfig, record, agent); err != nil { + if err := n.SendNotificationByConfig(ctx, &channelConfig, record, agent, maskIP); err != nil { n.logger.Error("发送通知失败", zap.String("channelType", channelConfig.Type), zap.Error(err), @@ -686,7 +727,7 @@ func (n *Notifier) SendWebhookByConfig(ctx context.Context, config map[string]in ActualValue: 0, FiredAt: time.Now().UnixMilli(), } - return n.sendWebhookByConfig(ctx, config, agent, record) + return n.sendWebhookByConfig(ctx, config, agent, record, false) } // SendTestNotification 发送测试通知(动态匹配通知渠道类型) @@ -719,7 +760,7 @@ func (n *Notifier) SendTestNotification(ctx context.Context, channelType string, ActualValue: 0, FiredAt: time.Now().UnixMilli(), } - return n.sendWebhookByConfig(ctx, config, agent, record) + return n.sendWebhookByConfig(ctx, config, agent, record, false) default: return fmt.Errorf("不支持的通知渠道类型: %s", channelType) } diff --git a/web/src/api/property.ts b/web/src/api/property.ts index 13d1716..b50a760 100644 --- a/web/src/api/property.ts +++ b/web/src/api/property.ts @@ -127,6 +127,7 @@ export interface AlertRules { // 全局告警配置 export interface AlertConfig { enabled: boolean; // 全局告警开关 + maskIP: boolean; // 是否在通知中打码 IP 地址 rules: AlertRules; } diff --git a/web/src/pages/Settings/AlertSettings.tsx b/web/src/pages/Settings/AlertSettings.tsx index 3810023..6fa1f23 100644 --- a/web/src/pages/Settings/AlertSettings.tsx +++ b/web/src/pages/Settings/AlertSettings.tsx @@ -1,17 +1,17 @@ -import {useEffect} from 'react'; -import {App, Button, Card, Form, InputNumber, Space, Switch} from 'antd'; -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; -import type {AlertConfig} from '@/api/property'; -import {getAlertConfig, saveAlertConfig} from '@/api/property'; -import {getErrorMessage} from '@/lib/utils'; +import { useEffect } from 'react'; +import { App, Button, Card, Form, InputNumber, Space, Switch } from 'antd'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { AlertConfig } from '@/api/property'; +import { getAlertConfig, saveAlertConfig } from '@/api/property'; +import { getErrorMessage } from '@/lib/utils'; const AlertSettings = () => { const [form] = Form.useForm(); - const {message: messageApi} = App.useApp(); + const { message: messageApi } = App.useApp(); const queryClient = useQueryClient(); // 获取全局告警配置 - const {data: configData, isLoading: configLoading} = useQuery({ + const { data: configData, isLoading: configLoading } = useQuery({ queryKey: ['alertConfig'], queryFn: getAlertConfig, }); @@ -28,7 +28,7 @@ const AlertSettings = () => { mutationFn: (config: AlertConfig) => saveAlertConfig(config), onSuccess: () => { messageApi.success('告警配置保存成功'); - queryClient.invalidateQueries({queryKey: ['alertConfig']}); + queryClient.invalidateQueries({ queryKey: ['alertConfig'] }); }, onError: (error: unknown) => { messageApi.error(getErrorMessage(error, '保存配置失败')); @@ -46,21 +46,29 @@ const AlertSettings = () => { - + + + + {/*告警规则*/} {[ - {key: 'cpu', title: 'CPU 告警规则', thresholdLabel: 'CPU 使用率阈值 (%)', max: 100}, - {key: 'memory', title: '内存告警规则', thresholdLabel: '内存使用率阈值 (%)', max: 100}, - {key: 'disk', title: '磁盘告警规则', thresholdLabel: '磁盘使用率阈值 (%)', max: 100}, - {key: 'network', title: '网速告警规则', thresholdLabel: '网速阈值 (MB/s)', max: 10000}, + { key: 'cpu', title: 'CPU 告警规则', thresholdLabel: 'CPU 使用率阈值 (%)', max: 100 }, + { key: 'memory', title: '内存告警规则', thresholdLabel: '内存使用率阈值 (%)', max: 100 }, + { key: 'disk', title: '磁盘告警规则', thresholdLabel: '磁盘使用率阈值 (%)', max: 100 }, + { key: 'network', title: '网速告警规则', thresholdLabel: '网速阈值 (MB/s)', max: 10000 }, ].map((rule) => ( - {({getFieldValue}) => { + {({ getFieldValue }) => { const enabled = getFieldValue(['rules', `${rule.key}Enabled`]); return (
@@ -70,7 +78,7 @@ const AlertSettings = () => { valuePropName="checked" className="mb-0" > - + { @@ -89,8 +97,8 @@ const AlertSettings = () => { name={['rules', `${rule.key}Duration`]} className="mb-0" > - +
); @@ -101,7 +109,7 @@ const AlertSettings = () => { - {({getFieldValue}) => { + {({ getFieldValue }) => { const enabled = getFieldValue(['rules', 'certEnabled']); return (
@@ -111,7 +119,7 @@ const AlertSettings = () => { valuePropName="checked" className="mb-0" > - + { @@ -134,7 +142,7 @@ const AlertSettings = () => { - {({getFieldValue}) => { + {({ getFieldValue }) => { const enabled = getFieldValue(['rules', 'serviceEnabled']); return (
@@ -144,7 +152,7 @@ const AlertSettings = () => { valuePropName="checked" className="mb-0" > - + { @@ -167,7 +175,7 @@ const AlertSettings = () => { - {({getFieldValue}) => { + {({ getFieldValue }) => { const enabled = getFieldValue(['rules', 'agentOfflineEnabled']); return (
@@ -177,7 +185,7 @@ const AlertSettings = () => { valuePropName="checked" className="mb-0" > - + {