mirror of https://github.com/openspug/spug
upgrade monitor module
parent
2ed651bd52
commit
540fc3511c
|
@ -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):
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue