Merge pull request #11 from yumusb/main
feat(notifier): 告警恢复增加持续时间,支持IP打码选项
This commit is contained in:
5 files changed
+101
-43
No files matched your search
@@ -65,6 +65,7 @@ type SystemConfig struct {
|
||||
// AlertConfig 全局告警配置
|
||||
type AlertConfig struct {
|
||||
Enabled bool `json:"enabled"` // 是否启用全局告警
|
||||
MaskIP bool `json:"maskIP"` // 是否在通知中打码 IP 地址
|
||||
Rules AlertRules `json:"rules"` // 告警规则
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ export interface AlertRules {
|
||||
// 全局告警配置
|
||||
export interface AlertConfig {
|
||||
enabled: boolean; // 全局告警开关
|
||||
maskIP: boolean; // 是否在通知中打码 IP 地址
|
||||
rules: AlertRules;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user