Skip to content

Commit 774d10f

Browse files
committed
# 添加飞书告警渠道;
# 钉钉支持设置secret;
1 parent 0e7b5ec commit 774d10f

File tree

6 files changed

+192
-42
lines changed

6 files changed

+192
-42
lines changed

spug_api/apps/alarm/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Alarm(models.Model, ModelMixin):
1515
('4', '邮件'),
1616
('5', '企业微信'),
1717
('6', '电话'),
18+
('7', '飞书'),
1819
)
1920
STATUS = (
2021
('1', '报警发生'),
@@ -71,6 +72,8 @@ class Contact(models.Model, ModelMixin):
7172
ding = models.CharField(max_length=255, null=True)
7273
wx_token = models.CharField(max_length=255, null=True)
7374
qy_wx = models.CharField(max_length=255, null=True)
75+
feishu = models.CharField(max_length=255, null=True)
76+
secret = models.TextField(null=True)
7477

7578
created_at = models.CharField(max_length=20, default=human_datetime)
7679
created_by = models.ForeignKey(User, models.PROTECT, related_name='+')

spug_api/apps/alarm/views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def post(self, request):
8585
Argument('ding', required=False),
8686
Argument('wx_token', required=False),
8787
Argument('qy_wx', required=False),
88+
Argument('feishu', required=False),
89+
Argument('secret', required=False),
8890
).parse(request.body)
8991
if error is None:
9092
if form.id:
@@ -116,9 +118,11 @@ def handle_test(request):
116118
if error is None:
117119
notify = Notification(None, '1', 'https://spug.cc', 'Spug官网(测试)', '这是一条测试告警信息', None)
118120
if form.mode == '3':
119-
notify.monitor_by_dd([form.value])
121+
notify.monitor_by_dd([(form.value, None)])
120122
elif form.mode == '4':
121123
notify.monitor_by_email([form.value])
122124
elif form.mode == '5':
123125
notify.monitor_by_qy_wx([form.value])
126+
elif form.mode == '7':
127+
notify.monitor_by_fs([(form.value, None)])
124128
return json_response(error=error)

spug_api/libs/spug.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@
99
from libs.push import push_server
1010
import requests
1111
import json
12+
import time
13+
import hmac
14+
import hashlib
15+
import base64
16+
from urllib.parse import urlencode
17+
18+
19+
def _gen_dd_sign(secret):
20+
timestamp = str(int(time.time() * 1000))
21+
string_to_sign = f'{timestamp}\n{secret}'
22+
hmac_code = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), digestmod=hashlib.sha256).digest()
23+
sign = base64.b64encode(hmac_code).decode('utf-8')
24+
return timestamp, sign
25+
26+
27+
def _gen_fs_sign(secret):
28+
timestamp = str(int(time.time()))
29+
string_to_sign = f'{timestamp}\n{secret}'
30+
hmac_code = hmac.new(string_to_sign.encode('utf-8'), b'', digestmod=hashlib.sha256).digest()
31+
sign = base64.b64encode(hmac_code).decode('utf-8')
32+
return timestamp, sign
1233

1334

1435
class Notification:
@@ -87,9 +108,41 @@ def monitor_by_dd(self, users):
87108
'isAtAll': True
88109
}
89110
}
90-
for url in users:
111+
for url, secret in users:
112+
if secret:
113+
timestamp, sign = _gen_dd_sign(secret)
114+
url = f'{url}&{urlencode({"timestamp": timestamp, "sign": sign})}'
91115
self.handle_request(url, data, 'dd')
92116

117+
def monitor_by_fs(self, users):
118+
title = '监控告警通知' if self.event == '1' else '告警恢复通知'
119+
content = [
120+
[{'tag': 'text', 'text': f'告警名称:{self.title}'}],
121+
[{'tag': 'text', 'text': f'告警对象:{self.target}'}],
122+
[{'tag': 'text', 'text': f'{"告警" if self.event == "1" else "恢复"}时间:{human_datetime()}'}],
123+
[{'tag': 'text', 'text': f'告警描述:{self.message}'}],
124+
]
125+
if self.event == '2':
126+
content.append([{'tag': 'text', 'text': f'持续时间:{self.duration}'}])
127+
content.append([{'tag': 'text', 'text': '来自 Spug运维平台'}])
128+
for url, secret in users:
129+
data = {
130+
'msg_type': 'post',
131+
'content': {
132+
'post': {
133+
'zh_cn': {
134+
'title': title,
135+
'content': content
136+
}
137+
}
138+
}
139+
}
140+
if secret:
141+
timestamp, sign = _gen_fs_sign(secret)
142+
data['timestamp'] = timestamp
143+
data['sign'] = sign
144+
self.handle_request(url, data, 'fs')
145+
93146
def monitor_by_qy_wx(self, users):
94147
color, title = ('warning', '监控告警通知') if self.event == '1' else ('info', '告警恢复通知')
95148
texts = [
@@ -149,7 +202,13 @@ def dispatch_monitor(self, modes):
149202
sms_ids = set(x for x in push_ids if x.startswith('sms_'))
150203
targets.update(sms_ids)
151204
elif mode == '3':
152-
users = set(x.ding for x in Contact.objects.filter(id__in=u_ids, ding__isnull=False))
205+
contacts = Contact.objects.filter(id__in=u_ids, ding__isnull=False)
206+
users = []
207+
for c in contacts:
208+
sec = None
209+
if c.secret:
210+
sec = json.loads(c.secret).get('ding')
211+
users.append((c.ding, sec))
153212
if not users:
154213
Notify.make_monitor_notify(
155214
'发送报警信息失败',
@@ -181,6 +240,21 @@ def dispatch_monitor(self, modes):
181240
elif mode == '6':
182241
voice_ids = set(x for x in push_ids if x.startswith('voice_'))
183242
targets.update(voice_ids)
243+
elif mode == '7':
244+
contacts = Contact.objects.filter(id__in=u_ids, feishu__isnull=False)
245+
users = []
246+
for c in contacts:
247+
sec = None
248+
if c.secret:
249+
sec = json.loads(c.secret).get('feishu')
250+
users.append((c.feishu, sec))
251+
if not users:
252+
Notify.make_monitor_notify(
253+
'发送报警信息失败',
254+
'未找到可用的通知对象,请确保设置了相关报警联系人的飞书。'
255+
)
256+
continue
257+
self.monitor_by_fs(users)
184258

185259
if targets:
186260
self.monitor_by_spug_push(targets)

spug_web/src/pages/alarm/contact/Form.js

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,82 @@
33
* Copyright (c) <spug.dev@gmail.com>
44
* Released under the AGPL-3.0 License.
55
*/
6-
import React, { useState } from 'react';
6+
import React, { useState, useMemo } from 'react';
77
import { observer } from 'mobx-react';
8-
import { Modal, Form, Input, Tooltip, message } from 'antd';
8+
import { Modal, Form, Input, Tooltip, Checkbox, Divider, message } from 'antd';
99
import { ThunderboltOutlined, LoadingOutlined } from '@ant-design/icons';
1010
import http from 'libs/http';
1111
import store from './store';
1212

13+
const channelConfig = [
14+
{
15+
key: 'email',
16+
label: '邮箱',
17+
fields: [
18+
{ name: 'email', label: '邮箱地址', placeholder: '请输入邮箱地址', testMode: '4' }
19+
]
20+
},
21+
{
22+
key: 'ding',
23+
label: '钉钉',
24+
fields: [
25+
{ name: 'ding', label: 'Webhook', placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=xxx', testMode: '3' },
26+
{ name: 'ding_secret', label: 'Secret', placeholder: 'SECxxxxxxxx', extra: '可选,机器人安全设置中的加签密钥' }
27+
],
28+
help: { text: '钉钉收不到通知?请参考', link: 'https://ops.spug.cc/docs/use-problem#use-dd', linkText: '官方文档' }
29+
},
30+
{
31+
key: 'feishu',
32+
label: '飞书',
33+
fields: [
34+
{ name: 'feishu', label: 'Webhook', placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx', testMode: '7' },
35+
{ name: 'feishu_secret', label: 'Secret', placeholder: 'xxxxxxxx', extra: '可选,机器人安全设置中的签名校验密钥' }
36+
]
37+
},
38+
{
39+
key: 'qy_wx',
40+
label: '企业微信',
41+
fields: [
42+
{ name: 'qy_wx', label: 'Webhook', placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx', testMode: '5' }
43+
]
44+
}
45+
];
46+
1347
export default observer(function () {
1448
const [form] = Form.useForm();
1549
const [loading, setLoading] = useState(false);
1650
const [testLoading, setTestLoading] = useState('0');
1751

52+
const initialChannels = useMemo(() => ({
53+
email: !!store.record.email,
54+
ding: !!store.record.ding,
55+
feishu: !!store.record.feishu,
56+
qy_wx: !!store.record.qy_wx,
57+
}), []);
58+
59+
const [channels, setChannels] = useState(initialChannels);
60+
61+
function handleChannelToggle(key, checked) {
62+
setChannels(prev => ({ ...prev, [key]: checked }));
63+
if (!checked) {
64+
const channel = channelConfig.find(c => c.key === key);
65+
if (channel) {
66+
const resetFields = channel.fields.map(f => f.name);
67+
form.resetFields(resetFields);
68+
}
69+
}
70+
}
71+
1872
function handleSubmit() {
1973
setLoading(true);
2074
const formData = form.getFieldsValue();
2175
formData['id'] = store.record.id;
76+
const secret = {};
77+
if (formData.ding_secret) secret.ding = formData.ding_secret;
78+
if (formData.feishu_secret) secret.feishu = formData.feishu_secret;
79+
delete formData.ding_secret;
80+
delete formData.feishu_secret;
81+
formData.secret = Object.keys(secret).length ? JSON.stringify(secret) : null;
2282
http.post('/api/alarm/contact/', formData)
2383
.then(res => {
2484
message.success('操作成功');
@@ -39,19 +99,15 @@ export default observer(function () {
3999
}
40100

41101
function Test(props) {
42-
return (
43-
<div style={{position: 'absolute', right: -30, top: 8}}>
44-
{testLoading === props.mode ? (
45-
<LoadingOutlined style={{fontSize: 18, color: '#faad14'}}/>
46-
) : (
47-
<Tooltip title="执行测试">
48-
<ThunderboltOutlined
49-
style={{fontSize: 18, color: '#faad14'}}
50-
onClick={() => handleTest(props.mode, props.name)}/>
51-
</Tooltip>
52-
)}
53-
</div>
54-
)
102+
return testLoading === props.mode ? (
103+
<LoadingOutlined style={{fontSize: 16, color: '#faad14'}}/>
104+
) : (
105+
<Tooltip title="执行测试">
106+
<ThunderboltOutlined
107+
style={{fontSize: 16, color: '#faad14', cursor: 'pointer'}}
108+
onClick={() => handleTest(props.mode, props.name)}/>
109+
</Tooltip>
110+
);
55111
}
56112

57113
return (
@@ -63,36 +119,47 @@ export default observer(function () {
63119
onCancel={() => store.formVisible = false}
64120
confirmLoading={loading}
65121
onOk={handleSubmit}>
66-
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
122+
<Form form={form} initialValues={{
123+
...store.record,
124+
ding_secret: store.record.secret ? JSON.parse(store.record.secret).ding : undefined,
125+
feishu_secret: store.record.secret ? JSON.parse(store.record.secret).feishu : undefined,
126+
}} labelCol={{span: 6}} wrapperCol={{span: 14}}>
67127
<Form.Item required name="name" label="姓名">
68128
<Input placeholder="请输入联系人姓名"/>
69129
</Form.Item>
70130
<Form.Item name="phone" label="手机号">
71131
<Input placeholder="请输入手机号"/>
72132
</Form.Item>
73-
<Form.Item label="邮箱">
74-
<Form.Item noStyle name="email">
75-
<Input placeholder="请输入邮箱地址"/>
76-
</Form.Item>
77-
<Test mode="4" name="email"/>
78-
</Form.Item>
79-
<Form.Item label="钉钉" extra={<span>
80-
钉钉收不到通知?请参考
81-
<a target="_blank" rel="noopener noreferrer"
82-
href="https://ops.spug.cc/docs/use-problem#use-dd">官方文档</a>
83-
</span>}>
84-
<Form.Item noStyle name="ding">
85-
<Input placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxx"/>
86-
</Form.Item>
87-
<Test mode="3" name="ding"/>
88-
</Form.Item>
89-
<Form.Item label="企业微信">
90-
<Form.Item noStyle name="qy_wx">
91-
<Input placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"/>
92-
</Form.Item>
93-
<Test mode="5" name="qy_wx"/>
94-
</Form.Item>
133+
<Divider orientation="left" style={{margin: '8px 0 16px'}}>通知渠道</Divider>
134+
{channelConfig.map(channel => (
135+
<div key={channel.key} style={{marginBottom: channels[channel.key] ? 16 : 4}}>
136+
<Form.Item wrapperCol={{offset: 6, span: 14}} style={{marginBottom: 0}}>
137+
<Checkbox
138+
checked={channels[channel.key]}
139+
onChange={e => handleChannelToggle(channel.key, e.target.checked)}
140+
>
141+
{channel.label}
142+
</Checkbox>
143+
</Form.Item>
144+
{channels[channel.key] && channel.fields.map(field => {
145+
const extra = field.extra || (channel.help && field === channel.fields[0] ? (
146+
<span>
147+
{channel.help.text}
148+
<a target="_blank" rel="noopener noreferrer" href={channel.help.link}>{channel.help.linkText}</a>
149+
</span>
150+
) : undefined);
151+
return (
152+
<Form.Item key={field.name} name={field.name} label={field.label} extra={extra}>
153+
<Input
154+
placeholder={field.placeholder}
155+
suffix={field.testMode ? <Test mode={field.testMode} name={field.name}/> : <span/>}
156+
/>
157+
</Form.Item>
158+
);
159+
})}
160+
</div>
161+
))}
95162
</Form>
96163
</Modal>
97164
)
98-
})
165+
})

spug_web/src/pages/alarm/contact/Table.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class ComTable extends React.Component {
5959
<Table.Column ellipsis hide title="钉钉" dataIndex="ding"/>
6060
<Table.Column ellipsis hide title="微信" dataIndex="wx_token"/>
6161
<Table.Column ellipsis hide title="企业微信" dataIndex="qy_wx"/>
62+
<Table.Column ellipsis hide title="飞书" dataIndex="feishu"/>
6263
{hasPermission('alarm.contact.edit|alarm.contact.del') && (
6364
<Table.Column title="操作" render={info => (
6465
<Action>

spug_web/src/pages/monitor/Step2.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const modeOptions = [
1919
{label: '邮件', 'value': '4'},
2020
{label: '钉钉', 'value': '3'},
2121
{label: '企业微信', 'value': '5'},
22+
{label: '飞书', 'value': '7'},
2223
];
2324

2425
export default observer(function () {

0 commit comments

Comments
 (0)