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

View File

@ -8,13 +8,18 @@ import subprocess
import platform import platform
import requests import requests
import logging import logging
import json
logging.captureWarnings(True) logging.captureWarnings(True)
def site_check(url): def site_check(url, limit):
try: try:
res = requests.get(url, timeout=10, verify=False) 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}' return 200 <= res.status_code < 400, f'返回状态码:{res.status_code}'
except Exception as e: except Exception as e:
return False, f'异常信息:{e}' return False, f'异常信息:{e}'
@ -58,9 +63,41 @@ def host_executor(host, command):
return False, f'异常信息:{e}' return False, f'异常信息:{e}'
def dispatch(tp, addr, extra, in_view=False): def monitor_worker_handler(job):
if not in_view: 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() 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': if tp == '1':
return site_check(addr) return site_check(addr)
elif tp == '2': elif tp == '2':

View File

@ -22,7 +22,8 @@ class Detection(models.Model, ModelMixin):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
type = models.CharField(max_length=2, choices=TYPES) type = models.CharField(max_length=2, choices=TYPES)
group = models.CharField(max_length=255, null=True) 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) extra = models.TextField(null=True)
desc = models.CharField(max_length=255, null=True) desc = models.CharField(max_length=255, null=True)
is_active = models.BooleanField(default=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['latest_status_alias'] = self.get_latest_status_display()
tmp['notify_mode'] = json.loads(self.notify_mode) tmp['notify_mode'] = json.loads(self.notify_mode)
tmp['notify_grp'] = json.loads(self.notify_grp) tmp['notify_grp'] = json.loads(self.notify_grp)
tmp['targets'] = json.loads(self.targets)
return tmp return tmp
def __repr__(self): def __repr__(self):

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ export default observer(function () {
function handleSubmit() { function handleSubmit() {
setLoading(true) setLoading(true)
const formData = form.getFieldsValue(); 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; formData['id'] = store.record.id;
http.post('/api/monitor/', formData) http.post('/api/monitor/', formData)
.then(() => { .then(() => {

View File

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