feat(notifier): 告警恢复增加持续时间,支持IP打码选项

- 告警恢复通知新增"持续时间"字段,自动格式化为秒/分/时
- 告警配置新增 maskIP 选项,开启后 IP 显示为 192.168.*.* 格式
- 前端告警设置页面增加 IP 打码开关
This commit is contained in:
yumusb committed 2025-12-16 16:38:55 +08:00
1 parent 726aee86f2
commit 4df385cd62
5 files changed
+101 -43

No files matched your search

+1
View File
@@ -65,6 +65,7 @@ type SystemConfig struct {
// AlertConfig 全局告警配置
type AlertConfig struct {
Enabled bool `json:"enabled"` // 是否启用全局告警
MaskIP bool `json:"maskIP"` // 是否在通知中打码 IP 地址
Rules AlertRules `json:"rules"` // 告警规则
}
+8 -1
View File
@@ -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))
}
}
+55 -14
View File
@@ -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)
}
+1
View File
@@ -127,6 +127,7 @@ export interface AlertRules {
// 全局告警配置
export interface AlertConfig {
enabled: boolean; // 全局告警开关
maskIP: boolean; // 是否在通知中打码 IP 地址
rules: AlertRules;
}
+36 -28
View File
@@ -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 = () => {
<Space direction="vertical" className="w-full">
<Card title="基本信息" type="inner">
<Form.Item label="启用告警" name="enabled" valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
</Form.Item>
<Form.Item
label="IP 打码"
name="maskIP"
valuePropName="checked"
tooltip="开启后,通知消息中的 IP 地址将显示为 192.168.*.* 格式"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
</Form.Item>
</Card>
{/*<Divider orientation="left">告警规则</Divider>*/}
{[
{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) => (
<Card key={rule.key} title={rule.title} type="inner">
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => {
{({ getFieldValue }) => {
const enabled = getFieldValue(['rules', `${rule.key}Enabled`]);
return (
<div className="flex items-center gap-8">
@@ -70,7 +78,7 @@ const AlertSettings = () => {
valuePropName="checked"
className="mb-0"
>
<Switch/>
<Switch />
</Form.Item>
<Form.Item
label={rule.thresholdLabel}
@@ -80,7 +88,7 @@ const AlertSettings = () => {
<InputNumber
min={0}
max={rule.max}
style={{width: '100%'}}
style={{ width: '100%' }}
disabled={!enabled}
/>
</Form.Item>
@@ -89,8 +97,8 @@ const AlertSettings = () => {
name={['rules', `${rule.key}Duration`]}
className="mb-0"
>
<InputNumber min={1} max={3600} style={{width: '100%'}}
disabled={!enabled}/>
<InputNumber min={1} max={3600} style={{ width: '100%' }}
disabled={!enabled} />
</Form.Item>
</div>
);
@@ -101,7 +109,7 @@ const AlertSettings = () => {
<Card title="HTTPS 证书告警规则" type="inner">
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => {
{({ getFieldValue }) => {
const enabled = getFieldValue(['rules', 'certEnabled']);
return (
<div className="flex items-center gap-8">
@@ -111,7 +119,7 @@ const AlertSettings = () => {
valuePropName="checked"
className="mb-0"
>
<Switch/>
<Switch />
</Form.Item>
<Form.Item
label="证书剩余天数阈值(天)"
@@ -122,7 +130,7 @@ const AlertSettings = () => {
<InputNumber
min={1}
max={365}
style={{width: '100%'}}
style={{ width: '100%' }}
disabled={!enabled}
/>
</Form.Item>
@@ -134,7 +142,7 @@ const AlertSettings = () => {
<Card title="服务下线告警规则" type="inner">
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => {
{({ getFieldValue }) => {
const enabled = getFieldValue(['rules', 'serviceEnabled']);
return (
<div className="flex items-center gap-8">
@@ -144,7 +152,7 @@ const AlertSettings = () => {
valuePropName="checked"
className="mb-0"
>
<Switch/>
<Switch />
</Form.Item>
<Form.Item
label="持续时间(秒)"
@@ -155,7 +163,7 @@ const AlertSettings = () => {
<InputNumber
min={1}
max={3600}
style={{width: '100%'}}
style={{ width: '100%' }}
disabled={!enabled}
/>
</Form.Item>
@@ -167,7 +175,7 @@ const AlertSettings = () => {
<Card title="探针离线告警规则" type="inner">
<Form.Item noStyle shouldUpdate>
{({getFieldValue}) => {
{({ getFieldValue }) => {
const enabled = getFieldValue(['rules', 'agentOfflineEnabled']);
return (
<div className="flex items-center gap-8">
@@ -177,7 +185,7 @@ const AlertSettings = () => {
valuePropName="checked"
className="mb-0"
>
<Switch/>
<Switch />
</Form.Item>
<Form.Item
label="持续时间(秒)"
@@ -188,7 +196,7 @@ const AlertSettings = () => {
<InputNumber
min={1}
max={3600}
style={{width: '100%'}}
style={{ width: '100%' }}
disabled={!enabled}
/>
</Form.Item>