upgrade monitor module

pull/330/head
vapao 2021-04-24 02:56:39 +08:00
parent 2ed651bd52
commit 540fc3511c
8 changed files with 112 additions and 70 deletions

View File

@ -6,6 +6,7 @@ from django.conf import settings
from django_redis import get_redis_connection
from concurrent.futures import ThreadPoolExecutor
from apps.schedule.executors import schedule_worker_handler
from apps.monitor.executors import monitor_worker_handler
import logging
MONITOR_WORKER_KEY = settings.MONITOR_WORKER_KEY
@ -24,7 +25,7 @@ class Worker:
if key.decode() == SCHEDULE_WORKER_KEY:
self._executor.submit(schedule_worker_handler, job)
else:
pass
self._executor.submit(monitor_worker_handler, job)
class Command(BaseCommand):

View File

@ -8,13 +8,18 @@ import subprocess
import platform
import requests
import logging
import json
logging.captureWarnings(True)
def site_check(url):
def site_check(url, limit):
try:
res = requests.get(url, timeout=10, verify=False)
if limit:
duration = int(res.elapsed.total_seconds() * 1000)
if duration > int(limit):
return False, f'响应时间:{duration}ms'
return 200 <= res.status_code < 400, f'返回状态码:{res.status_code}'
except Exception as e:
return False, f'异常信息:{e}'
@ -58,9 +63,41 @@ def host_executor(host, command):
return False, f'异常信息:{e}'
def dispatch(tp, addr, extra, in_view=False):
if not in_view:
def monitor_worker_handler(job):
print('enter: ', job)
task_id, tp, addr, extra = json.loads(job)
if tp == '1':
is_ok, message = site_check(addr, extra)
elif tp == '2':
is_ok, message = port_check(addr, extra)
elif tp == '5':
is_ok, message = ping_check(addr)
elif tp not in ('3', '4'):
is_ok, message = False, f'invalid monitor type for {tp!r}'
else:
close_old_connections()
command = f'ps -ef|grep -v grep|grep {extra!r}' if tp == '3' else extra
host = Host.objects.filter(pk=addr).first()
if not host:
is_ok, message = False, f'unknown host id for {addr!r}'
else:
is_ok, message = host_executor(host, command)
# is_notified = True if obj.latest_notify_time else False
# if obj.latest_status in [0, None] and is_ok is False:
# obj.latest_fault_time = int(time.time())
# if is_ok:
# obj.latest_notify_time = 0
# obj.fault_times = 0
# else:
# obj.fault_times += 1
# obj.latest_status = 0 if is_ok else 1
# obj.latest_run_time = human_datetime(event.scheduled_run_time)
# obj.save()
# self._handle_notify(obj, is_notified, out)
def dispatch(tp, addr, extra):
if tp == '1':
return site_check(addr)
elif tp == '2':

View File

@ -22,7 +22,8 @@ class Detection(models.Model, ModelMixin):
name = models.CharField(max_length=50)
type = models.CharField(max_length=2, choices=TYPES)
group = models.CharField(max_length=255, null=True)
addr = models.CharField(max_length=255)
addr = models.CharField(max_length=255) # 要删除的
targets = models.TextField()
extra = models.TextField(null=True)
desc = models.CharField(max_length=255, null=True)
is_active = models.BooleanField(default=True)
@ -48,6 +49,7 @@ class Detection(models.Model, ModelMixin):
tmp['latest_status_alias'] = self.get_latest_status_display()
tmp['notify_mode'] = json.loads(self.notify_mode)
tmp['notify_grp'] = json.loads(self.notify_grp)
tmp['targets'] = json.loads(self.targets)
return tmp
def __repr__(self):

View File

@ -10,7 +10,6 @@ from django.utils.functional import SimpleLazyObject
from django.db import close_old_connections
from apps.monitor.models import Detection
from apps.alarm.models import Alarm
from apps.monitor.executors import dispatch
from apps.monitor.utils import seconds_to_human
from apps.notify.models import Notify
from django.conf import settings
@ -20,6 +19,8 @@ import logging
import json
import time
MONITOR_WORKER_KEY = settings.MONITOR_WORKER_KEY
class Scheduler:
timezone = settings.TIME_ZONE
@ -28,7 +29,8 @@ class Scheduler:
self.scheduler = BackgroundScheduler(timezone=self.timezone, executors={'default': ThreadPoolExecutor(30)})
self.scheduler.add_listener(
self._handle_event,
EVENT_SCHEDULER_SHUTDOWN | EVENT_JOB_ERROR | EVENT_JOB_MAX_INSTANCES | EVENT_JOB_EXECUTED)
EVENT_SCHEDULER_SHUTDOWN | EVENT_JOB_ERROR | EVENT_JOB_MAX_INSTANCES
)
def _record_alarm(self, obj, status):
duration = seconds_to_human(time.time() - obj.latest_fault_time)
@ -97,15 +99,22 @@ class Scheduler:
obj.save()
self._handle_notify(obj, is_notified, out)
def _dispatch(self, task_id, tp, targets, extra):
close_old_connections()
Detection.objects.filter(pk=task_id).update(latest_run_time=human_datetime())
rds_cli = get_redis_connection()
for t in json.loads(targets):
rds_cli.rpush(MONITOR_WORKER_KEY, json.dumps([task_id, tp, t, extra]))
def _init(self):
self.scheduler.start()
for item in Detection.objects.filter(is_active=True):
trigger = IntervalTrigger(minutes=int(item.rate), timezone=self.timezone)
self.scheduler.add_job(
dispatch,
self._dispatch,
trigger,
id=str(item.id),
args=(item.type, item.addr, item.extra),
args=(item.id, item.type, item.targets, item.extra),
)
def run(self):
@ -119,10 +128,10 @@ class Scheduler:
if task.action in ('add', 'modify'):
trigger = IntervalTrigger(minutes=int(task.rate), timezone=self.timezone)
self.scheduler.add_job(
dispatch,
self._dispatch,
trigger,
id=str(task.id),
args=(task.type, task.addr, task.extra),
args=(task.id, task.type, task.targets, task.extra),
replace_existing=True
)
elif task.action == 'remove':

View File

@ -21,7 +21,7 @@ class DetectionView(View):
Argument('id', type=int, required=False),
Argument('name', help='请输入任务名称'),
Argument('group', help='请选择任务分组'),
Argument('addr', help='请输入监控地址'),
Argument('targets', type=list, filter=lambda x: len(x), help='请输入监控地址'),
Argument('type', filter=lambda x: x in dict(Detection.TYPES), help='请选择监控类型'),
Argument('extra', required=False),
Argument('desc', required=False),
@ -32,6 +32,7 @@ class DetectionView(View):
Argument('notify_mode', type=list, help='请选择报警方式'),
).parse(request.body)
if error is None:
form.targets = json.dumps(form.targets)
form.notify_grp = json.dumps(form.notify_grp)
form.notify_mode = json.dumps(form.notify_mode)
if form.id:
@ -63,7 +64,7 @@ class DetectionView(View):
if form.is_active:
task = Detection.objects.filter(pk=form.id).first()
message = {'id': form.id, 'action': 'add'}
message.update(task.to_dict(selects=('addr', 'extra', 'rate', 'type')))
message.update(task.to_dict(selects=('targets', 'extra', 'rate', 'type')))
else:
message = {'id': form.id, 'action': 'remove'}
rds_cli = get_redis_connection()
@ -86,10 +87,10 @@ class DetectionView(View):
def run_test(request):
form, error = JsonParser(
Argument('type', help='请选择监控类型'),
Argument('addr', help='请输入监控地址'),
Argument('targets', type=list, filter=lambda x: len(x), help='请输入监控地址'),
Argument('extra', required=False)
).parse(request.body)
if error is None:
is_success, message = dispatch(form.type, form.addr, form.extra, True)
is_success, message = dispatch(form.type, form.targets[0], form.extra)
return json_response({'is_success': is_success, 'message': message})
return json_response(error=error)

View File

@ -3,15 +3,16 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { observer } from 'mobx-react';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal, Form, Input, Select, Button } from 'antd';
import { Modal, Form, Input, Select, Button, message } from 'antd';
import TemplateSelector from '../exec/task/TemplateSelector';
import Selector from 'pages/host/Selector';
import { LinkButton, ACEditor } from 'components';
import { http, cleanCommand, hasHostPermission } from 'libs';
import { http, cleanCommand } from 'libs';
import store from './store';
import hostStore from '../host/store';
import lds from 'lodash';
const helpMap = {
'1': '返回HTTP状态码200-399则判定为正常其他为异常。',
@ -21,20 +22,12 @@ const helpMap = {
export default observer(function () {
const [loading, setLoading] = useState(false);
const [showTmp, setShowTmp] = useState(false);
useEffect(() => {
const { type, addr } = store.record;
if (type === '1' && addr) {
store.record.sitePrefix = addr.startsWith('http://') ? 'http://' : 'https://';
store.record.domain = store.record.addr.replace(store.record.sitePrefix, '')
}
}, [])
const [showSelector, setShowSelector] = useState(false);
function handleTest() {
setLoading(true)
const { type, sitePrefix, domain } = store.record;
if (type === '1') store.record.addr = sitePrefix + domain;
http.post('/api/monitor/test/', store.record, { timeout: 120000 })
const formData = lds.pick(store.record, ['type', 'targets', 'extra'])
http.post('/api/monitor/test/', formData, { timeout: 120000 })
.then(res => {
if (res.is_success) {
Modal.success({ content: res.message })
@ -47,7 +40,7 @@ export default observer(function () {
function handleChangeType(v) {
store.record.type = v;
store.record.addr = undefined;
store.record.targets = [];
store.record.extra = undefined;
};
@ -71,35 +64,30 @@ export default observer(function () {
})
}
const SiteBefore = (
<Select style={{ width: 90 }} value={store.record.sitePrefix} onChange={v => store.record.sitePrefix = v}>
<Select.Option value="http://">http://</Select.Option>
<Select.Option value="https://">https://</Select.Option>
</Select>
)
function canNext() {
const { type, addr, extra, domain, group } = store.record;
if (type === '1') {
return name && domain && group
} else if (type === '5') {
return name && addr && group
const { type, targets, extra, group } = store.record;
const is_verify = name && group && targets.length;
if (['2', '3', '4'].includes(type)) {
return is_verify && extra
} else {
return name && addr && extra && group
return is_verify
}
}
function toNext() {
const {type, extra} = store.record;
if (!Number(extra) > 0) {
if (type === '1' && extra) return message.error('请输入正确的响应时间')
if (type === '2') return message.error('请输入正确的端口号')
}
store.page += 1;
const { type, sitePrefix, domain } = store.record;
if (type === '1') store.record.addr = sitePrefix + domain;
}
function getStyle(t) {
return t.includes(store.record.type) ? { display: 'flex' } : { display: 'none' }
}
const { name, desc, type, addr, extra, domain, group } = store.record;
const { name, desc, type, targets, extra, group } = store.record;
return (
<Form labelCol={{ span: 6 }} wrapperCol={{ span: 14 }}>
<Form.Item required label="监控分组" style={{ marginBottom: 0 }}>
@ -114,7 +102,7 @@ export default observer(function () {
<Button type="link" onClick={handleAddGroup}>添加分组</Button>
</Form.Item>
</Form.Item>
<Form.Item label="监控类型" help={helpMap[type]}>
<Form.Item label="监控类型" tooltip={helpMap[type]}>
<Select placeholder="请选择监控类型" value={type} onChange={handleChangeType}>
<Select.Option value="1">站点检测</Select.Option>
<Select.Option value="2">端口检测</Select.Option>
@ -127,29 +115,27 @@ export default observer(function () {
<Input value={name} onChange={e => store.record.name = e.target.value} placeholder="请输入监控名称" />
</Form.Item>
<Form.Item required label="监控地址" style={getStyle(['1'])}>
<Input
value={domain}
addonBefore={SiteBefore}
placeholder="请输入监控地址"
onChange={e => store.record.domain = e.target.value} />
<Select
mode="tags"
value={targets}
onChange={v => store.record.targets = v}
placeholder="http(s)://开头,支持多个地址,每输入完成一个后按回车确认"
notFoundContent={null} />
</Form.Item>
<Form.Item required label="监控地址" style={getStyle(['2', '5'])}>
<Input value={addr} placeholder="请输入监控地址IP/域名)" onChange={e => store.record.addr = e.target.value} />
<Select
mode="tags"
value={targets}
onChange={v => store.record.targets = v}
placeholder="IP或域名支持多个地址每输入完成一个后按回车确认"
notFoundContent={null} />
</Form.Item>
<Form.Item required label="监控主机" style={getStyle(['3', '4'])}>
<Select
showSearch
value={addr}
placeholder="请选择主机"
optionFilterProp="children"
filterOption={(input, option) => option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
onChange={v => store.record.addr = v}>
{hostStore.records.filter(x => x.id === Number(addr) || hasHostPermission(x.id)).map(item => (
<Select.Option value={String(item.id)} key={item.id}>
{`${item.name}(${item.hostname}:${item.port})`}
</Select.Option>
))}
</Select>
{store.record.targets?.length > 0 && `已选择 ${store.record.targets.length}`}
<Button type="link" onClick={() => setShowSelector(true)}>选择主机</Button>
</Form.Item>
<Form.Item label="响应时间" style={getStyle(['1'])}>
<Input suffix="ms" placeholder="最长响应时间毫秒不设置则默认10秒超时" onChange={e => store.record.extra = e.target.value}/>
</Form.Item>
<Form.Item required label="检测端口" style={getStyle(['2'])}>
<Input value={extra} placeholder="请输入端口号" onChange={e => store.record.extra = e.target.value} />
@ -175,9 +161,15 @@ export default observer(function () {
<Form.Item wrapperCol={{ span: 14, offset: 6 }} style={{ marginTop: 12 }}>
<Button disabled={!canNext()} type="primary" onClick={toNext}>下一步</Button>
<Button disabled={false} type="link" loading={loading} onClick={handleTest}>执行测试</Button>
<Button disabled={!canNext()} type="link" loading={loading} onClick={handleTest}>执行测试</Button>
<span style={{color: '#888', fontSize: 12}}>Tips: 仅测试第一个监控地址</span>
</Form.Item>
{showTmp && <TemplateSelector onOk={v => store.record.extra += v} onCancel={() => setShowTmp(false)} />}
<Selector
visible={showSelector}
selectedRowKeys={[...store.record.targets]}
onCancel={() => setShowSelector(false)}
onOk={(_, ids) => store.record.targets = ids}/>
</Form>
)
})

View File

@ -34,7 +34,7 @@ export default observer(function () {
function handleSubmit() {
setLoading(true)
const formData = form.getFieldsValue();
Object.assign(formData, lds.pick(store.record, ['id', 'name', 'desc', 'addr', 'extra', 'type', 'group']))
Object.assign(formData, lds.pick(store.record, ['id', 'name', 'desc', 'targets', 'extra', 'type', 'group']))
formData['id'] = store.record.id;
http.post('/api/monitor/', formData)
.then(() => {

View File

@ -58,7 +58,7 @@ class Store {
};
showForm = (info) => {
info = info || {type: '1', sitePrefix: 'http://'};
info = info || {type: '1', targets: []};
this.page = 0;
this.record = lds.cloneDeep(info);
this.formVisible = true;