通知增加通信 IP 的展示,移除自定义 Webhook 的默认 json form 模版 fix #42
This commit is contained in:
1 parent
8d0e784d64
commit
3f3ddfea49
5 files changed
+66
-242
No files matched your search
@@ -28,8 +28,7 @@ type NotificationChannelConfig struct {
|
||||
// "url": "https://...",
|
||||
// "method": "POST", // 可选:GET, POST, PUT, PATCH, DELETE,默认 POST
|
||||
// "headers": {"key": "value"}, // 可选:自定义请求头
|
||||
// "bodyTemplate": "json" // 可选:json(默认), form, custom
|
||||
// "customBody": "" // 当 bodyTemplate 为 custom 时使用,支持变量替换
|
||||
// "customBody": "" // 自定义请求体模板,支持变量替换
|
||||
// }
|
||||
|
||||
// DNSProviderConfig DNS 服务商配置(存储在 Property 中)
|
||||
@@ -47,11 +46,10 @@ type DNSProviderConfig struct {
|
||||
|
||||
// WebhookConfig 自定义 Webhook 配置结构
|
||||
type WebhookConfig struct {
|
||||
URL string `json:"url"` // Webhook URL
|
||||
Method string `json:"method,omitempty"` // 请求方法,默认 POST
|
||||
Headers map[string]string `json:"headers,omitempty"` // 自定义请求头
|
||||
BodyTemplate string `json:"bodyTemplate,omitempty"` // 请求体模板:json, form, custom
|
||||
CustomBody string `json:"customBody,omitempty"` // 自定义请求体模板(支持变量)
|
||||
URL string `json:"url"` // Webhook URL
|
||||
Method string `json:"method,omitempty"` // 请求方法,默认 POST
|
||||
Headers map[string]string `json:"headers,omitempty"` // 自定义请求头
|
||||
CustomBody string `json:"customBody,omitempty"` // 自定义请求体模板(支持变量)
|
||||
}
|
||||
|
||||
type SystemConfig struct {
|
||||
|
||||
+26
-106
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -139,8 +138,11 @@ func maskIPAddress(ip string) string {
|
||||
return "****"
|
||||
}
|
||||
|
||||
func joinAgentIPs(ipv4 string, ipv6 string) string {
|
||||
func joinAgentIPs(ip string, ipv4 string, ipv6 string) string {
|
||||
parts := make([]string, 0, 2)
|
||||
if ip != "" {
|
||||
parts = append(parts, ip)
|
||||
}
|
||||
if ipv4 != "" {
|
||||
parts = append(parts, ipv4)
|
||||
}
|
||||
@@ -151,9 +153,13 @@ func joinAgentIPs(ipv4 string, ipv6 string) string {
|
||||
}
|
||||
|
||||
func formatAgentIP(agent *models.Agent, mask bool) string {
|
||||
ip := strings.TrimSpace(agent.IP)
|
||||
ipv4 := strings.TrimSpace(agent.IPv4)
|
||||
ipv6 := strings.TrimSpace(agent.IPv6)
|
||||
if mask {
|
||||
if ip != "" {
|
||||
ip = maskIPAddress(ip)
|
||||
}
|
||||
if ipv4 != "" {
|
||||
ipv4 = maskIPAddress(ipv4)
|
||||
}
|
||||
@@ -161,7 +167,7 @@ func formatAgentIP(agent *models.Agent, mask bool) string {
|
||||
ipv6 = maskIPAddress(ipv6)
|
||||
}
|
||||
}
|
||||
combined := joinAgentIPs(ipv4, ipv6)
|
||||
combined := joinAgentIPs(ip, ipv4, ipv6)
|
||||
if combined == "" {
|
||||
return "-"
|
||||
}
|
||||
@@ -534,11 +540,10 @@ func (n *Notifier) sendEmail(ctx context.Context, smtpHost string, smtpPort int,
|
||||
|
||||
// webhookConfig Webhook 配置
|
||||
type webhookConfig struct {
|
||||
URL string
|
||||
Method string
|
||||
Headers map[string]string
|
||||
BodyTemplate string
|
||||
CustomBody string
|
||||
URL string
|
||||
Method string
|
||||
Headers map[string]string
|
||||
CustomBody string
|
||||
}
|
||||
|
||||
// parseWebhookConfig 解析 Webhook 配置
|
||||
@@ -565,86 +570,21 @@ func parseWebhookConfig(config map[string]interface{}) (*webhookConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取请求体模板类型,默认 json
|
||||
bodyTemplate := "json"
|
||||
if bt, ok := config["bodyTemplate"].(string); ok && bt != "" {
|
||||
bodyTemplate = bt
|
||||
}
|
||||
|
||||
// 获取自定义请求体
|
||||
customBody, _ := config["customBody"].(string)
|
||||
|
||||
return &webhookConfig{
|
||||
URL: webhookURL,
|
||||
Method: method,
|
||||
Headers: headers,
|
||||
BodyTemplate: bodyTemplate,
|
||||
CustomBody: customBody,
|
||||
URL: webhookURL,
|
||||
Method: method,
|
||||
Headers: headers,
|
||||
CustomBody: customBody,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildJSONBody 构建 JSON 格式的请求体
|
||||
func (n *Notifier) buildJSONBody(agent *models.Agent, record *models.AlertRecord, message string) (io.Reader, error) {
|
||||
agentIP := joinAgentIPs(strings.TrimSpace(agent.IPv4), strings.TrimSpace(agent.IPv6))
|
||||
body := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"text": map[string]string{
|
||||
"content": message,
|
||||
},
|
||||
"agent": map[string]interface{}{
|
||||
"id": agent.ID,
|
||||
"name": agent.Name,
|
||||
"hostname": agent.Hostname,
|
||||
"ip": agentIP,
|
||||
"ipv4": agent.IPv4,
|
||||
"ipv6": agent.IPv6,
|
||||
},
|
||||
"alert": map[string]interface{}{
|
||||
"type": record.AlertType,
|
||||
"level": record.Level,
|
||||
"status": record.Status,
|
||||
"message": record.Message,
|
||||
"threshold": record.Threshold,
|
||||
"actualValue": record.ActualValue,
|
||||
"firedAt": record.FiredAt,
|
||||
"resolvedAt": record.ResolvedAt,
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化 JSON 失败: %w", err)
|
||||
}
|
||||
return bytes.NewReader(data), nil
|
||||
}
|
||||
|
||||
// buildFormBody 构建 Form 表单格式的请求体
|
||||
func (n *Notifier) buildFormBody(agent *models.Agent, record *models.AlertRecord, message string) io.Reader {
|
||||
agentIP := joinAgentIPs(strings.TrimSpace(agent.IPv4), strings.TrimSpace(agent.IPv6))
|
||||
formData := url.Values{}
|
||||
formData.Set("message", message)
|
||||
formData.Set("agent_id", agent.ID)
|
||||
formData.Set("agent_name", agent.Name)
|
||||
formData.Set("agent_hostname", agent.Hostname)
|
||||
formData.Set("agent_ip", agentIP)
|
||||
formData.Set("agent_ipv4", agent.IPv4)
|
||||
formData.Set("agent_ipv6", agent.IPv6)
|
||||
formData.Set("alert_type", record.AlertType)
|
||||
formData.Set("alert_level", record.Level)
|
||||
formData.Set("alert_status", record.Status)
|
||||
formData.Set("alert_message", record.Message)
|
||||
formData.Set("threshold", fmt.Sprintf("%.2f", record.Threshold))
|
||||
formData.Set("actual_value", fmt.Sprintf("%.2f", record.ActualValue))
|
||||
formData.Set("fired_at", fmt.Sprintf("%d", record.FiredAt))
|
||||
if record.ResolvedAt > 0 {
|
||||
formData.Set("resolved_at", fmt.Sprintf("%d", record.ResolvedAt))
|
||||
}
|
||||
return strings.NewReader(formData.Encode())
|
||||
}
|
||||
|
||||
// buildCustomBody 构建自定义模板格式的请求体
|
||||
func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertRecord, message, customBody string) (io.Reader, error) {
|
||||
func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertRecord, message, customBody string, maskIP bool) (io.Reader, error) {
|
||||
if customBody == "" {
|
||||
return nil, fmt.Errorf("使用 custom 模板时必须提供 customBody")
|
||||
return nil, fmt.Errorf("必须提供自定义请求体模板")
|
||||
}
|
||||
|
||||
// 使用 fasttemplate 进行变量替换
|
||||
@@ -669,7 +609,7 @@ func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertReco
|
||||
case "agent.hostname":
|
||||
v = agent.Hostname
|
||||
case "agent.ip":
|
||||
v = joinAgentIPs(strings.TrimSpace(agent.IPv4), strings.TrimSpace(agent.IPv6))
|
||||
v = agent.IP
|
||||
case "agent.ipv4":
|
||||
v = agent.IPv4
|
||||
case "agent.ipv6":
|
||||
@@ -758,34 +698,14 @@ func (n *Notifier) sendCustomWebhook(ctx context.Context, config map[string]inte
|
||||
// 构建消息内容
|
||||
message := n.buildMessage(agent, record, maskIP)
|
||||
|
||||
// 根据模板类型构建请求体
|
||||
var reqBody io.Reader
|
||||
var contentType string
|
||||
|
||||
switch cfg.BodyTemplate {
|
||||
case "json":
|
||||
reqBody, err = n.buildJSONBody(agent, record, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentType = "application/json"
|
||||
|
||||
case "form":
|
||||
reqBody = n.buildFormBody(agent, record, message)
|
||||
contentType = "application/x-www-form-urlencoded"
|
||||
|
||||
case "custom":
|
||||
reqBody, err = n.buildCustomBody(agent, record, message, cfg.CustomBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentType = "text/plain"
|
||||
|
||||
default:
|
||||
return fmt.Errorf("不支持的 bodyTemplate: %s", cfg.BodyTemplate)
|
||||
// 构建自定义请求体
|
||||
reqBody, err := n.buildCustomBody(agent, record, message, cfg.CustomBody, maskIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送 HTTP 请求
|
||||
// 发送 HTTP 请求,使用 application/json 作为默认 Content-Type
|
||||
contentType := "application/json"
|
||||
return n.sendHTTPRequest(ctx, cfg.Method, cfg.URL, reqBody, cfg.Headers, contentType)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ const (
|
||||
PropertyIDNotificationChannels = "notification_channels"
|
||||
// PropertyIDSystemConfig 系统配置的固定 ID
|
||||
PropertyIDSystemConfig = "system_config"
|
||||
// PropertyIDMetricsConfig 指标配置的固定 ID
|
||||
PropertyIDMetricsConfig = "metrics_config"
|
||||
// PropertyIDPublicIPConfig 公网 IP 采集配置的固定 ID
|
||||
PropertyIDPublicIPConfig = "public_ip_config"
|
||||
// PropertyIDAlertConfig 告警配置的固定 ID
|
||||
|
||||
@@ -103,7 +103,6 @@ const NotificationChannels = () => {
|
||||
formValues.webhookEnabled = channel.enabled;
|
||||
formValues.webhookUrl = channel.config?.url || '';
|
||||
formValues.webhookMethod = channel.config?.method || 'POST';
|
||||
formValues.webhookBodyTemplate = channel.config?.bodyTemplate || 'json';
|
||||
formValues.webhookCustomBody = channel.config?.customBody || '';
|
||||
|
||||
// 解析 headers 为数组形式方便编辑
|
||||
@@ -223,7 +222,6 @@ const NotificationChannels = () => {
|
||||
config: {
|
||||
url: values.webhookUrl || '',
|
||||
method: values.webhookMethod || 'POST',
|
||||
bodyTemplate: values.webhookBodyTemplate || 'json',
|
||||
customBody: values.webhookCustomBody || '',
|
||||
headers: Object.keys(headersObj).length > 0 ? headersObj : undefined,
|
||||
},
|
||||
@@ -715,8 +713,7 @@ const NotificationChannels = () => {
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.webhookEnabled !== currentValues.webhookEnabled ||
|
||||
prevValues.webhookBodyTemplate !== currentValues.webhookBodyTemplate
|
||||
prevValues.webhookEnabled !== currentValues.webhookEnabled
|
||||
}
|
||||
>
|
||||
{({getFieldValue}) =>
|
||||
@@ -750,51 +747,24 @@ const NotificationChannels = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 请求体模板 */}
|
||||
{/* 自定义请求体 */}
|
||||
<Form.Item
|
||||
label="请求体模板"
|
||||
name="webhookBodyTemplate"
|
||||
tooltip="选择请求体的格式"
|
||||
label="自定义请求体"
|
||||
name="webhookCustomBody"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入自定义请求体模板'
|
||||
}
|
||||
]}
|
||||
tooltip="支持变量替换,可用变量见下方说明"
|
||||
>
|
||||
<Select
|
||||
placeholder="选择请求体模板"
|
||||
options={[
|
||||
{
|
||||
label: 'JSON (默认)',
|
||||
value: 'json'
|
||||
},
|
||||
{
|
||||
label: 'Form 表单',
|
||||
value: 'form'
|
||||
},
|
||||
{
|
||||
label: '自定义模板',
|
||||
value: 'custom'
|
||||
},
|
||||
]}
|
||||
<Input.TextArea
|
||||
rows={6}
|
||||
placeholder='示例: {"alert": "{{alert.message}}", "host": "{{agent.hostname}}"}'
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 自定义请求体 */}
|
||||
{getFieldValue('webhookBodyTemplate') === 'custom' && (
|
||||
<Form.Item
|
||||
label="自定义请求体"
|
||||
name="webhookCustomBody"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入自定义请求体模板'
|
||||
}
|
||||
]}
|
||||
tooltip="支持变量替换,可用变量见下方说明"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={6}
|
||||
placeholder='示例: {"alert": "{{alert.message}}", "host": "{{agent.hostname}}"}'
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 自定义请求头 */}
|
||||
<Form.Item label="自定义请求头"
|
||||
tooltip="添加自定义 HTTP 请求头">
|
||||
|
||||
@@ -6,97 +6,35 @@ const NotificationCustomHelp = () => {
|
||||
const items: CollapseProps['items'] = [
|
||||
{
|
||||
key: 'help',
|
||||
label: '请求体格式说明',
|
||||
label: '自定义请求体模板说明',
|
||||
children: <div className={'space-y-3 text-sm'}>
|
||||
{/* JSON 格式说明 */}
|
||||
<div className={'space-y-1'}>
|
||||
<strong>1. JSON 格式 (默认):</strong>
|
||||
<div className={'text-gray-600 text-xs'}>
|
||||
发送 <code className={'bg-gray-100 px-1 rounded'}>application/json</code> 格式的数据
|
||||
</div>
|
||||
<pre className={'border p-2 rounded-md text-xs mt-1 bg-gray-50'}>
|
||||
{JSON.stringify({
|
||||
"msg_type": "text",
|
||||
"text": {"content": "告警消息内容"},
|
||||
"agent": {
|
||||
"id": "agent-id",
|
||||
"name": "探针名称",
|
||||
"hostname": "主机名",
|
||||
"ip": "192.168.1.1"
|
||||
},
|
||||
"alert": {
|
||||
"type": "cpu",
|
||||
"level": "warning",
|
||||
"status": "firing",
|
||||
"message": "CPU使用率过高",
|
||||
"threshold": 80,
|
||||
"actualValue": 85.5,
|
||||
"firedAt": 1234567890000,
|
||||
"resolvedAt": 0
|
||||
}
|
||||
}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Form 表单格式说明 */}
|
||||
<div className={'space-y-1'}>
|
||||
<strong>2. Form 表单格式:</strong>
|
||||
<div className={'text-gray-600 text-xs'}>
|
||||
发送 <code
|
||||
className={'bg-gray-100 px-1 rounded'}>application/x-www-form-urlencoded</code> 格式的数据
|
||||
</div>
|
||||
<div className={'border p-2 rounded-md text-xs mt-1 bg-gray-50'}>
|
||||
<div className={'font-semibold mb-1'}>包含以下字段:</div>
|
||||
<div className={'grid grid-cols-2 gap-x-4 gap-y-1'}>
|
||||
<div>• <code>message</code> - 告警消息</div>
|
||||
<div>• <code>agent_id</code> - 探针ID</div>
|
||||
<div>• <code>agent_name</code> - 探针名称</div>
|
||||
<div>• <code>agent_hostname</code> - 主机名</div>
|
||||
<div>• <code>agent_ip</code> - IP地址(合并)</div>
|
||||
<div>• <code>agent_ipv4</code> - IPv4 地址</div>
|
||||
<div>• <code>agent_ipv6</code> - IPv6 地址</div>
|
||||
<div>• <code>alert_type</code> - 告警类型</div>
|
||||
<div>• <code>alert_level</code> - 告警级别</div>
|
||||
<div>• <code>alert_status</code> - 告警状态</div>
|
||||
<div>• <code>alert_message</code> - 告警详情</div>
|
||||
<div>• <code>threshold</code> - 阈值</div>
|
||||
<div>• <code>actual_value</code> - 当前值</div>
|
||||
<div>• <code>fired_at</code> - 触发时间(格式化)</div>
|
||||
<div>• <code>resolved_at</code> - 恢复时间(格式化)</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自定义模板说明 */}
|
||||
<div className={'space-y-1'}>
|
||||
<strong>3. 自定义模板:</strong>
|
||||
<div className={'text-gray-600 text-xs'}>
|
||||
支持变量替换,Content-Type 为 <code
|
||||
className={'bg-gray-100 px-1 rounded'}>text/plain</code>
|
||||
<div className={'text-gray-600 dark:text-gray-400 text-xs'}>
|
||||
支持变量替换,Content-Type 为 <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>application/json</code>
|
||||
</div>
|
||||
<div className={'border p-2 rounded-md text-xs mt-1 bg-gray-50'}>
|
||||
<div className={'border dark:border-gray-700 p-2 rounded-md text-xs mt-1 bg-gray-50 dark:bg-gray-800'}>
|
||||
<div className={'font-semibold mb-1'}>可用变量:</div>
|
||||
<div className={'grid grid-cols-2 gap-x-4 gap-y-1'}>
|
||||
<div>• <code>{`{{message}}`}</code> - 告警消息</div>
|
||||
<div>• <code>{`{{agent.id}}`}</code> - 探针ID</div>
|
||||
<div>• <code>{`{{agent.name}}`}</code> - 探针名称</div>
|
||||
<div>• <code>{`{{agent.hostname}}`}</code> - 主机名</div>
|
||||
<div>• <code>{`{{agent.ip}}`}</code> - IP地址(合并)</div>
|
||||
<div>• <code>{`{{agent.ipv4}}`}</code> - IPv4 地址</div>
|
||||
<div>• <code>{`{{agent.ipv6}}`}</code> - IPv6 地址</div>
|
||||
<div>• <code>{`{{alert.type}}`}</code> - 告警类型</div>
|
||||
<div>• <code>{`{{alert.level}}`}</code> - 告警级别</div>
|
||||
<div>• <code>{`{{alert.status}}`}</code> - 告警状态</div>
|
||||
<div>• <code>{`{{alert.message}}`}</code> - 告警消息</div>
|
||||
<div>• <code>{`{{alert.threshold}}`}</code> - 阈值</div>
|
||||
<div>• <code>{`{{alert.actualValue}}`}</code> - 当前值</div>
|
||||
<div>• <code>{`{{alert.firedAt}}`}</code> - 触发时间(格式化)</div>
|
||||
<div>• <code>{`{{alert.resolvedAt}}`}</code> - 恢复时间(格式化)</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{message}}`}</code> - 告警消息</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.id}}`}</code> - 探针ID</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.name}}`}</code> - 探针名称</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.hostname}}`}</code> - 主机名</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.ip}}`}</code> - IP地址(合并)</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.ipv4}}`}</code> - IPv4 地址</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.ipv6}}`}</code> - IPv6 地址</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.type}}`}</code> - 告警类型</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.level}}`}</code> - 告警级别</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.status}}`}</code> - 告警状态</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.message}}`}</code> - 告警消息</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.threshold}}`}</code> - 阈值</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.actualValue}}`}</code> - 当前值</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.firedAt}}`}</code> - 触发时间(格式化)</div>
|
||||
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.resolvedAt}}`}</code> - 恢复时间(格式化)</div>
|
||||
</div>
|
||||
<div className={'mt-2 pt-2 border-t'}>
|
||||
<div className={'mt-2 pt-2 border-t dark:border-gray-700'}>
|
||||
<div className={'font-semibold mb-1'}>示例:</div>
|
||||
<pre className={'text-xs'}>
|
||||
<pre className={'text-xs bg-gray-100 dark:bg-gray-700 p-2 rounded'}>
|
||||
{`{
|
||||
"alert": "{{alert.message}}",
|
||||
"host": "{{agent.hostname}}",
|
||||
|
||||
Reference in new issue
Block a user