mm_monitor 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/example/index.js +48 -0
- package/index.js +16 -143
- package/lib/monitor.js +146 -0
- package/lib/tools/health/index.js +99 -0
- package/lib/tools/index.js +16 -0
- package/lib/tools/monitor/index.js +299 -0
- package/package.json +40 -33
- package/test.js +32 -13
- package/tool/health/index.js +7 -0
- package/tool/health/tool.json +46 -0
- package/tool/monitor/index.js +7 -0
- package/tool/monitor/tool.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# mm_monitor
|
|
2
|
+
|
|
3
|
+
mm_monitor 是一个网站运行状态监听告警模块,按固定心跳间隔轮询检测目标 URL 是否可正常访问(HTTP 200),检测到异常时自动通过短信(短信宝接口)与邮件通知运维人员,并支持异常后的冷却重跑,避免连续重复告警。适合对线上服务做轻量级存活监控。
|
|
4
|
+
|
|
5
|
+
## 项目信息
|
|
6
|
+
|
|
7
|
+
- **当前版本**: 1.0.6
|
|
8
|
+
- **更新日期**: 2026-10-04
|
|
9
|
+
- **Gitee地址**: https://gitee.com/qiuwenwu91/mm_monitor
|
|
10
|
+
- **依赖**:
|
|
11
|
+
- mm_https ^1.6.7(HTTP 客户端,用于探活请求)
|
|
12
|
+
- mm_phone ^1.0.2(短信发送,采用短信宝 API 接口)
|
|
13
|
+
- mm_email ^1.0.8(邮件发送)
|
|
14
|
+
|
|
15
|
+
## 特点
|
|
16
|
+
|
|
17
|
+
- 心跳轮询检测多个监听项,间隔可配置(默认 30 秒)。
|
|
18
|
+
- 探活失败后间隔 1 秒自动复检一次,两次均失败才告警,减少误报。
|
|
19
|
+
- 同一轮异常只告警一次(内部 `sended` 标志去重)。
|
|
20
|
+
- 告警后停止轮询,进入冷却期(默认 3600 秒)后自动重新运行,避免连续通知轰炸。
|
|
21
|
+
- 短信 + 邮件双通道通知,各通道可通过 `state` 开关独立启用。
|
|
22
|
+
- 单项检测异常被 try/catch 捕获并记录日志,不影响其余监听项。
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install mm_monitor
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 基本使用
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
const Monitor = require('mm_monitor');
|
|
34
|
+
|
|
35
|
+
const monitor = new Monitor({
|
|
36
|
+
phone: { // 短信通道(短信宝)
|
|
37
|
+
state: 1, // 是否启用
|
|
38
|
+
username: 'your-sms-user',
|
|
39
|
+
password: 'your-sms-password'
|
|
40
|
+
},
|
|
41
|
+
email: { // 邮件通道
|
|
42
|
+
state: 1,
|
|
43
|
+
host: 'smtp.qq.com',
|
|
44
|
+
username: 'ops@qq.com',
|
|
45
|
+
password: 'your-smtp-authcode'
|
|
46
|
+
},
|
|
47
|
+
listen: [
|
|
48
|
+
{
|
|
49
|
+
name: 'AI考勤系统', // 网站名称
|
|
50
|
+
url: 'https://example.com/api/health', // 探活地址
|
|
51
|
+
email: 'ops@qq.com', // 告警邮箱
|
|
52
|
+
phone: '13800000000', // 告警手机号
|
|
53
|
+
content: '【考勤系统】服务器异常,请尽快进行维护!' // 告警内容
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
interval: 30, // 心跳间隔(秒)
|
|
57
|
+
interval_again: 3600 // 告警后冷却时长(秒)
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
monitor.run(); // 启动监听
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API 说明
|
|
64
|
+
|
|
65
|
+
导出唯一类 `Monitor`。
|
|
66
|
+
|
|
67
|
+
### 构造函数
|
|
68
|
+
|
|
69
|
+
`new Monitor(config)` — `config` 会与内置默认配置合并(`Object.assign`),并自动调用 `init` 初始化短信、邮件通道实例。
|
|
70
|
+
|
|
71
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| phone | `Object` | `{ state: 1, username: '', password: '' }` | 短信通道配置,传给 `mm_phone`(短信宝接口);`state` 为是否启用 |
|
|
74
|
+
| email | `Object` | `{ state: 1, host: 'smtp.qq.com', username: '', password: '' }` | 邮件通道配置,传给 `mm_email`;`password` 一般为 SMTP 授权码 |
|
|
75
|
+
| listen | `Array` | `[]` | 监听项列表,每项含 `name`(网站名称)、`url`(探活地址)、`email`(告警邮箱)、`phone`(告警手机号)、`content`(告警内容) |
|
|
76
|
+
| interval | `Number` | `30` | 心跳轮询间隔,单位秒 |
|
|
77
|
+
| interval_again | `Number` | `3600` | 出现异常并告警后的冷却时长,单位秒,到期后自动重新 `run` |
|
|
78
|
+
|
|
79
|
+
### 实例方法
|
|
80
|
+
|
|
81
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| init | `await init(config)` | `Promise` | 合并配置到 `this.config`,创建 `mm_phone` 短信实例与 `mm_email` 邮件实例。构造函数已自动调用 |
|
|
84
|
+
| checkServer | `await checkServer(url)` | `Boolean` | 用 `mm_https` GET 请求 `url`,响应状态码为 200 返回 `true`,否则 `false` |
|
|
85
|
+
| notiy | `await notiy(o)` | `Promise` | 发送告警(方法名拼写以源码为准)。解构 `o` 的 `phone`、`email`、`title`、`content`:先 `sms.send(phone, content)`,再 `email.send(title || '服务器异常', content, null, email)` |
|
|
86
|
+
| loop | `await loop()` | `Promise` | 执行一轮巡检:逐项 `checkServer`,失败则 1 秒后复检;仍未恢复且本轮未告警过则调用 `notiy`。若本轮发生告警,停止定时器并在 `interval_again` 秒后重新 `run` |
|
|
87
|
+
| run | `await run()` | `Promise` | 重置告警标志,按 `interval` 秒周期启动 `setInterval` 循环执行 `loop` |
|
|
88
|
+
|
|
89
|
+
> 注意:运行依赖全局工具对象 `$`(`$.log` 日志、`$.sleep` 延时),需在已加载相应基础库的环境中使用。
|
|
90
|
+
|
|
91
|
+
## 使用示例
|
|
92
|
+
|
|
93
|
+
### 场景一:从配置文件启动监听
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const Monitor = require('mm_monitor');
|
|
97
|
+
|
|
98
|
+
const config = './config.json'.loadJson(); // 含 phone/email/listen/interval 等
|
|
99
|
+
const monitor = new Monitor(config);
|
|
100
|
+
monitor.run();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 场景二:监听多个站点
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const monitor = new Monitor({
|
|
107
|
+
phone: { state: 1, username: 'sms-user', password: 'sms-pass' },
|
|
108
|
+
email: { state: 1, host: 'smtp.qq.com', username: 'ops@qq.com', password: 'authcode' },
|
|
109
|
+
listen: [
|
|
110
|
+
{ name: '官网', url: 'https://www.example.com', email: 'ops@qq.com', phone: '13800000000', content: '【官网】服务器异常,请尽快维护!' },
|
|
111
|
+
{ name: 'API服务', url: 'https://api.example.com/health', email: 'ops@qq.com', phone: '13800000000', content: '【API】服务不可用,请尽快维护!' }
|
|
112
|
+
],
|
|
113
|
+
interval: 60
|
|
114
|
+
});
|
|
115
|
+
monitor.run();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 场景三:仅手动单次探活(不启动定时器)
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
const Monitor = require('mm_monitor');
|
|
122
|
+
|
|
123
|
+
const monitor = new Monitor({});
|
|
124
|
+
(async () => {
|
|
125
|
+
const ok = await monitor.checkServer('https://www.example.com');
|
|
126
|
+
console.log(ok ? '服务正常' : '服务异常');
|
|
127
|
+
})();
|
|
128
|
+
```
|
package/example/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mm_monitor 最小可运行示例
|
|
3
|
+
* @description 演示 Monitor 类的实例化、工厂获取实例与核心方法存在性。
|
|
4
|
+
* 注意:不会真正发起网站检测与短信/邮件通知;phone/email 配置为示例假值,实际使用时请替换为真实配置。
|
|
5
|
+
*/
|
|
6
|
+
const { Monitor, monitorAdmin } = require('../index.js');
|
|
7
|
+
|
|
8
|
+
/** 示例假配置(实际使用时替换为真实的手机号/邮箱通知配置) */
|
|
9
|
+
const CONFIG = {
|
|
10
|
+
phone: {
|
|
11
|
+
state: 0,
|
|
12
|
+
username: 'example_user',
|
|
13
|
+
password: 'example_password'
|
|
14
|
+
},
|
|
15
|
+
email: {
|
|
16
|
+
state: 0,
|
|
17
|
+
host: 'smtp.qq.com',
|
|
18
|
+
username: 'example@qq.com',
|
|
19
|
+
password: 'example_code'
|
|
20
|
+
},
|
|
21
|
+
listen: [],
|
|
22
|
+
interval: 60
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 主函数
|
|
27
|
+
*/
|
|
28
|
+
function main() {
|
|
29
|
+
console.log('== mm_monitor 示例开始 ==');
|
|
30
|
+
|
|
31
|
+
// 实例化(仅初始化配置与通知器,不启动监听)
|
|
32
|
+
const monitor = new Monitor(CONFIG);
|
|
33
|
+
console.log('实例化 Monitor 成功');
|
|
34
|
+
console.log('监听列表:', Array.isArray(monitor.config.listen) ? monitor.config.listen.length + ' 项' : '无效');
|
|
35
|
+
|
|
36
|
+
// 通过工厂函数获取按作用域缓存的实例
|
|
37
|
+
const inst = monitorAdmin('example', { listen: [] });
|
|
38
|
+
console.log('monitorAdmin 获取实例:', inst instanceof Monitor);
|
|
39
|
+
|
|
40
|
+
// 核心方法存在性检查(不真正执行,避免联网检测与发送通知)
|
|
41
|
+
const METHODS = ['init', 'checkServer', 'notiy', 'loop', 'run'];
|
|
42
|
+
const missing = METHODS.filter((name) => typeof inst[name] !== 'function');
|
|
43
|
+
console.log('核心方法检查:', missing.length === 0 ? '全部存在' : '缺失: ' + missing.join(','));
|
|
44
|
+
|
|
45
|
+
console.log('== mm_monitor 示例结束 ==');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main();
|
package/index.js
CHANGED
|
@@ -1,143 +1,16 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
state: 1,
|
|
18
|
-
// 用户名
|
|
19
|
-
username: '',
|
|
20
|
-
// 密码,一般为授权码
|
|
21
|
-
password: ''
|
|
22
|
-
},
|
|
23
|
-
email: {
|
|
24
|
-
// 是否启用
|
|
25
|
-
state: 1,
|
|
26
|
-
// 服务器地址
|
|
27
|
-
host: 'smtp.qq.com',
|
|
28
|
-
// 用户名
|
|
29
|
-
username: 'xxxxxx@qq.com',
|
|
30
|
-
// 密码,一般为授权码
|
|
31
|
-
password: ''
|
|
32
|
-
},
|
|
33
|
-
listen: [
|
|
34
|
-
// {
|
|
35
|
-
// // 网站名称
|
|
36
|
-
// name: "AI考勤系统",
|
|
37
|
-
// // URL地址
|
|
38
|
-
// url: "https://xxx.xxx.com/xxx/xxx/xxx",
|
|
39
|
-
// // 推送到邮箱
|
|
40
|
-
// email: "xxx@qq.com",
|
|
41
|
-
// // 发送短信到
|
|
42
|
-
// phone: "xxx",
|
|
43
|
-
// // 推送的内容
|
|
44
|
-
// content: "【xxx】服务器异常,请尽快进行维护!--xxx系统"
|
|
45
|
-
// }
|
|
46
|
-
],
|
|
47
|
-
// 心跳间隔时长
|
|
48
|
-
interval: 30,
|
|
49
|
-
interval_again: 3600
|
|
50
|
-
}
|
|
51
|
-
// 是否已发送,用于避免重复发送
|
|
52
|
-
this.sended = false;
|
|
53
|
-
this.init(config);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 初始化
|
|
59
|
-
* @param {Object} config 配置
|
|
60
|
-
*/
|
|
61
|
-
Monitor.prototype.init = async function(config) {
|
|
62
|
-
Object.assign(this.config, config);
|
|
63
|
-
this.sms = new SMS(this.config.phone);
|
|
64
|
-
this.email = new Email(this.config.email);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 检测访问是否正常
|
|
69
|
-
* @param {String} url 服务器地址
|
|
70
|
-
*/
|
|
71
|
-
Monitor.prototype.checkServer = async function(url) {
|
|
72
|
-
var http = new Http();
|
|
73
|
-
var res = await http.get(url);
|
|
74
|
-
if (res.status == 200) {
|
|
75
|
-
return true;
|
|
76
|
-
} else {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 通知
|
|
83
|
-
* @param {Object} o 通知的对象信息
|
|
84
|
-
*/
|
|
85
|
-
Monitor.prototype.notiy = async function(o) {
|
|
86
|
-
var {
|
|
87
|
-
phone,
|
|
88
|
-
email,
|
|
89
|
-
title,
|
|
90
|
-
content
|
|
91
|
-
} = o;
|
|
92
|
-
await this.sms.send(phone, content);
|
|
93
|
-
await this.email.send(title || "服务器异常", content, null, email);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 循环检测
|
|
98
|
-
*/
|
|
99
|
-
Monitor.prototype.loop = async function() {
|
|
100
|
-
var lt = this.config.listen;
|
|
101
|
-
var hasBug = false;
|
|
102
|
-
for (var i = 0; i < lt.length; i++) {
|
|
103
|
-
var o = lt[i];
|
|
104
|
-
try {
|
|
105
|
-
var bl = await this.checkServer(o.url);
|
|
106
|
-
if (!bl) {
|
|
107
|
-
$.sleep(1000);
|
|
108
|
-
bl = await this.checkServer(o.url);
|
|
109
|
-
if (!bl) {
|
|
110
|
-
$.log.debug("发生异常", (new Date()).toStr('yyyy-MM-dd hh:mm:ss'), o);
|
|
111
|
-
if (!this.sended) {
|
|
112
|
-
this.sended = true;
|
|
113
|
-
await this.notiy(o);
|
|
114
|
-
hasBug = true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} catch (err) {
|
|
119
|
-
$.log.error(err);
|
|
120
|
-
}
|
|
121
|
-
$.sleep(1000);
|
|
122
|
-
}
|
|
123
|
-
if (hasBug) {
|
|
124
|
-
// 如果出现异常,则先暂停运行
|
|
125
|
-
clearInterval(this.timer);
|
|
126
|
-
// 等过了N久后再运行,避免连续通知
|
|
127
|
-
setTimeout(async () => {
|
|
128
|
-
await this.run();
|
|
129
|
-
}, this.config.interval_again * 1000);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 运行监听
|
|
135
|
-
*/
|
|
136
|
-
Monitor.prototype.run = async function() {
|
|
137
|
-
this.sended = false;
|
|
138
|
-
this.timer = setInterval(async () => {
|
|
139
|
-
await this.loop();
|
|
140
|
-
}, this.config.interval * 1000);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
module.exports = Monitor;
|
|
1
|
+
const Monitor = require('./lib/monitor');
|
|
2
|
+
const { reg } = require('mm_admin');
|
|
3
|
+
|
|
4
|
+
const admin = reg('mm_monitor');
|
|
5
|
+
admin.setClass(Monitor);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
Monitor,
|
|
9
|
+
admin,
|
|
10
|
+
monitorAdmin(scope, config = {}) {
|
|
11
|
+
return admin.get(scope || 'os', config);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/* mm-tool-logic-export 工具能力层:其它模块可 require("<pkg>").tools.<name> 直接引用 */
|
|
15
|
+
Object.defineProperty(module.exports, 'tools', { enumerable: true, configurable: true,
|
|
16
|
+
get: function () { return require('./lib/tools'); } });
|
package/lib/monitor.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const Http = require('mm_https');
|
|
2
|
+
const { sleep, date } = require('mm_helper');
|
|
3
|
+
const SMS = require('mm_phone').Phone;
|
|
4
|
+
const Email = require('mm_email').Mail;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 短信通知类: 采用短信宝的API接口
|
|
8
|
+
*/
|
|
9
|
+
class Monitor {
|
|
10
|
+
/**
|
|
11
|
+
* 构造函数
|
|
12
|
+
* @param {Object} config 配置参数
|
|
13
|
+
*/
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = {
|
|
16
|
+
phone: {
|
|
17
|
+
// 是否启用
|
|
18
|
+
state: 1,
|
|
19
|
+
// 用户名
|
|
20
|
+
username: '',
|
|
21
|
+
// 密码,一般为授权码
|
|
22
|
+
password: ''
|
|
23
|
+
},
|
|
24
|
+
email: {
|
|
25
|
+
// 是否启用
|
|
26
|
+
state: 1,
|
|
27
|
+
// 服务器地址
|
|
28
|
+
host: 'smtp.qq.com',
|
|
29
|
+
// 用户名
|
|
30
|
+
username: 'xxxxxx@qq.com',
|
|
31
|
+
// 密码,一般为授权码
|
|
32
|
+
password: ''
|
|
33
|
+
},
|
|
34
|
+
listen: [
|
|
35
|
+
// {
|
|
36
|
+
// // 网站名称
|
|
37
|
+
// name: "AI考勤系统",
|
|
38
|
+
// // URL地址
|
|
39
|
+
// url: "https://xxx.xxx.com/xxx/xxx/xxx",
|
|
40
|
+
// // 推送到邮箱
|
|
41
|
+
// email: "xxx@qq.com",
|
|
42
|
+
// // 发送短信到
|
|
43
|
+
// phone: "xxx",
|
|
44
|
+
// // 推送的内容
|
|
45
|
+
// content: "【xxx】服务器异常,请尽快进行维护!--xxx系统"
|
|
46
|
+
// }
|
|
47
|
+
],
|
|
48
|
+
// 心跳间隔时长
|
|
49
|
+
interval: 30,
|
|
50
|
+
interval_again: 3600
|
|
51
|
+
}
|
|
52
|
+
// 是否已发送,用于避免重复发送
|
|
53
|
+
this.sended = false;
|
|
54
|
+
// 日志记录器(可经 config.logger 注入,缺省使用 console)
|
|
55
|
+
this._logger = config && config.logger ? config.logger : console;
|
|
56
|
+
this.init(config);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 初始化
|
|
62
|
+
* @param {Object} config 配置
|
|
63
|
+
*/
|
|
64
|
+
Monitor.prototype.init = async function(config) {
|
|
65
|
+
Object.assign(this.config, config);
|
|
66
|
+
this.sms = new SMS(this.config.phone);
|
|
67
|
+
this.email = new Email(this.config.email);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 检测访问是否正常
|
|
72
|
+
* @param {String} url 服务器地址
|
|
73
|
+
*/
|
|
74
|
+
Monitor.prototype.checkServer = async function(url) {
|
|
75
|
+
var http = new Http();
|
|
76
|
+
var res = await http.get(url);
|
|
77
|
+
if (res.status == 200) {
|
|
78
|
+
return true;
|
|
79
|
+
} else {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 通知
|
|
86
|
+
* @param {Object} o 通知的对象信息
|
|
87
|
+
*/
|
|
88
|
+
Monitor.prototype.notiy = async function(o) {
|
|
89
|
+
var {
|
|
90
|
+
phone,
|
|
91
|
+
email,
|
|
92
|
+
title,
|
|
93
|
+
content
|
|
94
|
+
} = o;
|
|
95
|
+
await this.sms.send(phone, content);
|
|
96
|
+
await this.email.send(title || "服务器异常", content, null, email);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 循环检测
|
|
101
|
+
*/
|
|
102
|
+
Monitor.prototype.loop = async function() {
|
|
103
|
+
var lt = this.config.listen;
|
|
104
|
+
var hasBug = false;
|
|
105
|
+
for (var i = 0; i < lt.length; i++) {
|
|
106
|
+
var o = lt[i];
|
|
107
|
+
try {
|
|
108
|
+
var bl = await this.checkServer(o.url);
|
|
109
|
+
if (!bl) {
|
|
110
|
+
await sleep(1000);
|
|
111
|
+
bl = await this.checkServer(o.url);
|
|
112
|
+
if (!bl) {
|
|
113
|
+
this._logger.debug("发生异常", date.toStr(new Date(), 'yyyy-MM-dd hh:mm:ss'), o);
|
|
114
|
+
if (!this.sended) {
|
|
115
|
+
this.sended = true;
|
|
116
|
+
await this.notiy(o);
|
|
117
|
+
hasBug = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
this._logger.error(err);
|
|
123
|
+
}
|
|
124
|
+
await sleep(1000);
|
|
125
|
+
}
|
|
126
|
+
if (hasBug) {
|
|
127
|
+
// 如果出现异常,则先暂停运行
|
|
128
|
+
clearInterval(this.timer);
|
|
129
|
+
// 等过了N久后再运行,避免连续通知
|
|
130
|
+
setTimeout(async () => {
|
|
131
|
+
await this.run();
|
|
132
|
+
}, this.config.interval_again * 1000);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 运行监听
|
|
138
|
+
*/
|
|
139
|
+
Monitor.prototype.run = async function() {
|
|
140
|
+
this.sended = false;
|
|
141
|
+
this.timer = setInterval(async () => {
|
|
142
|
+
await this.loop();
|
|
143
|
+
}, this.config.interval * 1000);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = Monitor;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 健康检查观察器
|
|
3
|
+
* 通过 os/process 模块读取真实系统状态
|
|
4
|
+
*/
|
|
5
|
+
var os = require('os');
|
|
6
|
+
|
|
7
|
+
function getDir() {
|
|
8
|
+
return $?.run_path || process.cwd();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function main(params, ctx, options) {
|
|
12
|
+
var checks = params.checks || ['memory', 'cpu', 'disk'];
|
|
13
|
+
var results = [];
|
|
14
|
+
var all_healthy = true;
|
|
15
|
+
|
|
16
|
+
for (var i = 0; i < checks.length; i++) {
|
|
17
|
+
var name = checks[i];
|
|
18
|
+
var result = { name: name };
|
|
19
|
+
|
|
20
|
+
switch (name) {
|
|
21
|
+
case 'memory': {
|
|
22
|
+
var total = os.totalmem();
|
|
23
|
+
var free = os.freemem();
|
|
24
|
+
var used_pct = Math.round((1 - free / total) * 100);
|
|
25
|
+
result.status = used_pct > 90 ? 'warn' : 'ok';
|
|
26
|
+
result.value = used_pct + '%';
|
|
27
|
+
result.detail = { total: total, free: free, used_pct: used_pct };
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'cpu': {
|
|
31
|
+
var cpus = os.cpus();
|
|
32
|
+
var load = os.loadavg();
|
|
33
|
+
result.status = load[0] > cpus.length * 0.8 ? 'warn' : 'ok';
|
|
34
|
+
result.value = (load[0] || 0).toFixed(2);
|
|
35
|
+
result.detail = {
|
|
36
|
+
cores: cpus.length,
|
|
37
|
+
load_1m: load[0],
|
|
38
|
+
load_5m: load[1],
|
|
39
|
+
load_15m: load[2]
|
|
40
|
+
};
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case 'disk': {
|
|
44
|
+
try {
|
|
45
|
+
var stat = require('fs').statfsSync(getDir());
|
|
46
|
+
var used_pct = Math.round((1 - stat.bfree / stat.blocks) * 100);
|
|
47
|
+
result.status = used_pct > 90 ? 'warn' : 'ok';
|
|
48
|
+
result.value = used_pct + '%';
|
|
49
|
+
result.detail = {
|
|
50
|
+
total_blocks: stat.blocks,
|
|
51
|
+
free_blocks: stat.bfree,
|
|
52
|
+
used_pct: used_pct
|
|
53
|
+
};
|
|
54
|
+
} catch (e) {
|
|
55
|
+
result.status = 'error';
|
|
56
|
+
result.value = '';
|
|
57
|
+
result.detail = { error: e.message };
|
|
58
|
+
all_healthy = false;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'uptime': {
|
|
63
|
+
var uptime = process.uptime();
|
|
64
|
+
result.status = 'ok';
|
|
65
|
+
result.value = Math.floor(uptime) + 's';
|
|
66
|
+
result.detail = { uptime_seconds: uptime };
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case 'process': {
|
|
70
|
+
var mem = process.memoryUsage();
|
|
71
|
+
result.status = mem.heapUsed / mem.heapTotal > 0.9 ? 'warn' : 'ok';
|
|
72
|
+
result.value = Math.round(mem.heapUsed / 1024 / 1024) + 'MB';
|
|
73
|
+
result.detail = {
|
|
74
|
+
pid: process.pid,
|
|
75
|
+
heap_used: mem.heapUsed,
|
|
76
|
+
heap_total: mem.heapTotal,
|
|
77
|
+
rss: mem.rss
|
|
78
|
+
};
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
default: {
|
|
82
|
+
result.status = 'unknown';
|
|
83
|
+
result.value = '';
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.status !== 'ok') {
|
|
89
|
+
all_healthy = false;
|
|
90
|
+
}
|
|
91
|
+
results.push(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ctx.output.health = results;
|
|
95
|
+
ctx.output = { healthy: all_healthy, checks: results };
|
|
96
|
+
return ctx.output;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { main };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
/** 模块能力层:懒加载工具实现,避免模块入口被 require 时加载全部实现 */
|
|
5
|
+
module.exports = new Proxy({}, {
|
|
6
|
+
get(_t, key) {
|
|
7
|
+
if (typeof key !== 'string') return undefined;
|
|
8
|
+
if (!fs.existsSync(path.join(__dirname, key, 'index.js'))) return undefined;
|
|
9
|
+
return require('./' + key);
|
|
10
|
+
},
|
|
11
|
+
has(_t, key) {
|
|
12
|
+
return typeof key === 'string' && fs.existsSync(path.join(__dirname, key, 'index.js'));
|
|
13
|
+
},
|
|
14
|
+
ownKeys() { return fs.readdirSync(__dirname, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); },
|
|
15
|
+
getOwnPropertyDescriptor(_t, key) { return this.has(_t, key) ? { enumerable: true, configurable: true } : undefined; }
|
|
16
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const child_process = require('child_process');
|
|
5
|
+
const Http = require('mm_https');
|
|
6
|
+
|
|
7
|
+
const ACTIONS = ['get_system', 'get_processes', 'check_port', 'check_service'];
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
/**
|
|
11
|
+
* 系统监控
|
|
12
|
+
* @param {object} params 监控参数
|
|
13
|
+
* @param {string} params.action 操作类型
|
|
14
|
+
* @param {number} params.top 进程数量
|
|
15
|
+
* @param {string} params.host 主机地址
|
|
16
|
+
* @param {number} params.port 端口号
|
|
17
|
+
* @param {number} params.timeout 超时时间(毫秒)
|
|
18
|
+
* @param {string} params.url 服务地址
|
|
19
|
+
* @returns {object} 监控结果
|
|
20
|
+
*/
|
|
21
|
+
async monitor(params) {
|
|
22
|
+
let { action } = params;
|
|
23
|
+
if (!action) {
|
|
24
|
+
throw new TypeError('操作类型(action)不能为空');
|
|
25
|
+
}
|
|
26
|
+
if (ACTIONS.indexOf(action) === -1) {
|
|
27
|
+
throw new Error('不支持的操作类型: ' + action);
|
|
28
|
+
}
|
|
29
|
+
switch (action) {
|
|
30
|
+
case 'get_system':
|
|
31
|
+
return await this.getSystem();
|
|
32
|
+
case 'get_processes':
|
|
33
|
+
return await this.getProc(params.top);
|
|
34
|
+
case 'check_port':
|
|
35
|
+
return await this.checkPort(params.host, params.port, params.timeout);
|
|
36
|
+
case 'check_service':
|
|
37
|
+
return await this.checkService(params.url, params.timeout);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取系统信息
|
|
43
|
+
* @returns {object} 系统信息
|
|
44
|
+
*/
|
|
45
|
+
async getSystem() {
|
|
46
|
+
return {
|
|
47
|
+
hostname: os.hostname(),
|
|
48
|
+
platform: os.platform(),
|
|
49
|
+
arch: os.arch(),
|
|
50
|
+
uptime: os.uptime(),
|
|
51
|
+
cpu: this.getCpu(),
|
|
52
|
+
memory: this.getMemory(),
|
|
53
|
+
disk: this.getDisk(),
|
|
54
|
+
network: this.getNetwork(),
|
|
55
|
+
loadavg: os.loadavg()
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 获取CPU使用率
|
|
61
|
+
* @returns {object} CPU信息
|
|
62
|
+
*/
|
|
63
|
+
getCpu() {
|
|
64
|
+
let cpus = os.cpus();
|
|
65
|
+
let total = 0;
|
|
66
|
+
let idle = 0;
|
|
67
|
+
for (let cpu of cpus) {
|
|
68
|
+
for (let type in cpu.times) {
|
|
69
|
+
total += cpu.times[type];
|
|
70
|
+
}
|
|
71
|
+
idle += cpu.times.idle;
|
|
72
|
+
}
|
|
73
|
+
let usage = total > 0 ? (1 - idle / total) * 100 : 0;
|
|
74
|
+
return {
|
|
75
|
+
cores: cpus.length,
|
|
76
|
+
model: cpus.length > 0 ? cpus[0].model : '',
|
|
77
|
+
usage_percent: parseFloat(usage.toFixed(1))
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 获取内存使用情况
|
|
83
|
+
* @returns {object} 内存信息
|
|
84
|
+
*/
|
|
85
|
+
getMemory() {
|
|
86
|
+
let total = os.totalmem();
|
|
87
|
+
let free = os.freemem();
|
|
88
|
+
return {
|
|
89
|
+
total: total,
|
|
90
|
+
free: free,
|
|
91
|
+
used: total - free,
|
|
92
|
+
usage_percent: total > 0 ? parseFloat(((1 - free / total) * 100).toFixed(1)) : 0
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 获取磁盘使用情况
|
|
98
|
+
* @returns {Array} 磁盘信息列表
|
|
99
|
+
*/
|
|
100
|
+
getDisk() {
|
|
101
|
+
let list = [];
|
|
102
|
+
try {
|
|
103
|
+
if (typeof fs.statfsSync === 'function') {
|
|
104
|
+
let stat = fs.statfsSync(process.cwd());
|
|
105
|
+
let total = stat.blocks * stat.bsize;
|
|
106
|
+
let free = stat.bavail * stat.bsize;
|
|
107
|
+
list.push({
|
|
108
|
+
path: process.cwd(),
|
|
109
|
+
total: total,
|
|
110
|
+
free: free,
|
|
111
|
+
usage_percent: total > 0 ? parseFloat(((1 - free / total) * 100).toFixed(1)) : 0
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
void error;
|
|
116
|
+
// 获取磁盘失败时返回空列表
|
|
117
|
+
}
|
|
118
|
+
return list;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 获取网络接口信息
|
|
123
|
+
* @returns {Array} 网络接口列表
|
|
124
|
+
*/
|
|
125
|
+
getNetwork() {
|
|
126
|
+
let list = [];
|
|
127
|
+
let ifaces = os.networkInterfaces();
|
|
128
|
+
for (let key in ifaces) {
|
|
129
|
+
let addrs = ifaces[key];
|
|
130
|
+
for (let addr of addrs) {
|
|
131
|
+
if (addr.family === 'IPv4' && !addr.internal) {
|
|
132
|
+
list.push({ name: key, address: addr.address, mac: addr.mac });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return list;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 获取进程列表
|
|
141
|
+
* @param {number} top 进程数量
|
|
142
|
+
* @returns {object} 进程列表
|
|
143
|
+
*/
|
|
144
|
+
async getProc(top) {
|
|
145
|
+
let limit = top || 20;
|
|
146
|
+
let is_win = process.platform === 'win32';
|
|
147
|
+
let cmd = is_win ? 'tasklist /FO CSV' : 'ps -eo pid,pcpu,pmem,comm --sort=-pcpu';
|
|
148
|
+
try {
|
|
149
|
+
let output = child_process.execSync(cmd, { encoding: 'utf-8', maxBuffer: 5242880 }); // eslint-disable-line mm_eslint/variable-name
|
|
150
|
+
let list = this.parseProc(output, is_win).slice(0, limit);
|
|
151
|
+
return { list: list, count: list.length };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
void error;
|
|
154
|
+
return { list: [], count: 0 };
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 解析进程输出
|
|
160
|
+
* @param {string} output 命令输出
|
|
161
|
+
* @param {boolean} is_win 是否Windows
|
|
162
|
+
* @returns {Array} 进程列表
|
|
163
|
+
*/
|
|
164
|
+
parseProc(output, is_win) {
|
|
165
|
+
let list = [];
|
|
166
|
+
let lines = output.trim().split('\n');
|
|
167
|
+
for (let i = 1; i < lines.length; i++) {
|
|
168
|
+
let line = lines[i].trim();
|
|
169
|
+
if (!line) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let item = is_win ? this.parseWinLine(line) : this.parseUnixLine(line);
|
|
173
|
+
if (item) {
|
|
174
|
+
list.push(item);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return list;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 解析Windows进程行
|
|
182
|
+
* @param {string} line 行内容
|
|
183
|
+
* @returns {object|null} 进程信息
|
|
184
|
+
*/
|
|
185
|
+
parseWinLine(line) {
|
|
186
|
+
let cols = line.split('","');
|
|
187
|
+
if (cols.length < 3) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
let name = cols[0].replace(/"/g, '');
|
|
191
|
+
let pid = parseInt(cols[1], 10);
|
|
192
|
+
let mem = cols[cols.length - 1].replace(/"/g, '').replace(/[^\d]/g, '');
|
|
193
|
+
return {
|
|
194
|
+
pid: pid,
|
|
195
|
+
name: name,
|
|
196
|
+
memory_kb: parseInt(mem, 10) || 0
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 解析Unix进程行
|
|
202
|
+
* @param {string} line 行内容
|
|
203
|
+
* @returns {object|null} 进程信息
|
|
204
|
+
*/
|
|
205
|
+
parseUnixLine(line) {
|
|
206
|
+
let parts = line.trim().split(/\s+/);
|
|
207
|
+
if (parts.length < 4) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
pid: parseInt(parts[0], 10),
|
|
212
|
+
cpu_percent: parseFloat(parts[1]),
|
|
213
|
+
mem_percent: parseFloat(parts[2]),
|
|
214
|
+
name: parts.slice(3).join(' ')
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 检测端口是否开放
|
|
220
|
+
* @param {string} host 主机地址
|
|
221
|
+
* @param {number} port 端口号
|
|
222
|
+
* @param {number} timeout 超时时间
|
|
223
|
+
* @returns {object} 检测结果
|
|
224
|
+
*/
|
|
225
|
+
checkPort(host, port, timeout) {
|
|
226
|
+
if (!host) {
|
|
227
|
+
throw new TypeError('主机(host)不能为空');
|
|
228
|
+
}
|
|
229
|
+
if (!port) {
|
|
230
|
+
throw new TypeError('端口(port)不能为空');
|
|
231
|
+
}
|
|
232
|
+
let timeout_ms = timeout || 3000;
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
let socket = net.connect(port, host);
|
|
235
|
+
socket.setTimeout(timeout_ms);
|
|
236
|
+
socket.on('connect', () => {
|
|
237
|
+
socket.destroy();
|
|
238
|
+
resolve({ open: true, host: host, port: port });
|
|
239
|
+
});
|
|
240
|
+
socket.on('timeout', () => {
|
|
241
|
+
socket.destroy();
|
|
242
|
+
resolve({ open: false, host: host, port: port });
|
|
243
|
+
});
|
|
244
|
+
socket.on('error', () => {
|
|
245
|
+
resolve({ open: false, host: host, port: port });
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 检测HTTP服务连通性
|
|
252
|
+
* @param {string} url 服务地址
|
|
253
|
+
* @param {number} timeout 超时时间
|
|
254
|
+
* @returns {object} 检测结果
|
|
255
|
+
*/
|
|
256
|
+
async checkService(url, timeout) {
|
|
257
|
+
if (!url) {
|
|
258
|
+
throw new TypeError('服务地址(url)不能为空');
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
let http = new Http({ rejectUnauthorized: false }); // eslint-disable-line mm_eslint/variable-name
|
|
262
|
+
let start = Date.now();
|
|
263
|
+
let res = await http.get(url);
|
|
264
|
+
let cost = Date.now() - start;
|
|
265
|
+
let status = typeof res.status === 'number' ? res.status : (res.status && res.status.code);
|
|
266
|
+
return {
|
|
267
|
+
ok: status >= 200 && status < 500,
|
|
268
|
+
url: url,
|
|
269
|
+
status: status || 0,
|
|
270
|
+
ms: cost
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { ok: false, url: url, status: 0, ms: 0, msg: error.message };
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 主函数
|
|
279
|
+
* @param {object} params 参数
|
|
280
|
+
* @returns {object} 执行结果
|
|
281
|
+
*/
|
|
282
|
+
async main(params, ctx, options) {
|
|
283
|
+
try {
|
|
284
|
+
let result = await this.monitor(params);
|
|
285
|
+
return { code: 0, msg: '操作成功', data: result };
|
|
286
|
+
} catch (error) {
|
|
287
|
+
return {
|
|
288
|
+
code: 10000,
|
|
289
|
+
msg: '系统监控失败!原因:' + error.message,
|
|
290
|
+
data: null,
|
|
291
|
+
error: {
|
|
292
|
+
name: error.name,
|
|
293
|
+
message: error.message,
|
|
294
|
+
stack: error.stack
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
package/package.json
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mm_monitor",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "超级美眉监听器,用于监听网站运行是否异常并给运维人员发短信、邮箱通知",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "node test.js"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://gitee.com/qiuwenwu91/mm_monitor.git"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "mm_monitor",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "超级美眉监听器,用于监听网站运行是否异常并给运维人员发短信、邮箱通知",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://gitee.com/qiuwenwu91/mm_monitor.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mm_mod",
|
|
15
|
+
"mm_crosscut",
|
|
16
|
+
"mm_alerting",
|
|
17
|
+
"mvc",
|
|
18
|
+
"web",
|
|
19
|
+
"view",
|
|
20
|
+
"model",
|
|
21
|
+
"controller",
|
|
22
|
+
"render",
|
|
23
|
+
"template"
|
|
24
|
+
],
|
|
25
|
+
"layer": "infrastructure",
|
|
26
|
+
"group": "observability",
|
|
27
|
+
"author": "邱文武",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://gitee.com/qiuwenwu91/mm_monitor/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://gitee.com/qiuwenwu91/mm_monitor#readme",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"mm_admin": "^1.1.0",
|
|
35
|
+
"mm_email": "^1.1.0",
|
|
36
|
+
"mm_helper": "^1.0.3",
|
|
37
|
+
"mm_https": "^1.7.1",
|
|
38
|
+
"mm_phone": "^1.0.2"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/test.js
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const mod = require('./index.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 运行 mm_monitor 模块测试
|
|
7
|
+
* 仅验证导出结构与实例化,不调用 run() 以避免真实联网与发送通知
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
async function runTest() {
|
|
11
|
+
// 验证导出结构
|
|
12
|
+
assert.strictEqual(typeof mod.Monitor, 'function', '应导出 Monitor');
|
|
13
|
+
assert.ok(mod.admin, '应导出 admin');
|
|
14
|
+
assert.strictEqual(typeof mod.monitorAdmin, 'function', '应导出 monitorAdmin 函数');
|
|
15
|
+
|
|
16
|
+
// 实例化 Monitor(不调用 run,避免真实请求)
|
|
17
|
+
const monitor = new mod.Monitor({ listen: [], interval: 60 });
|
|
18
|
+
assert.ok(monitor instanceof mod.Monitor, 'Monitor 应可实例化');
|
|
19
|
+
assert.ok(monitor.config, '实例应包含 config');
|
|
20
|
+
assert.ok(Array.isArray(monitor.config.listen), 'config 应包含 listen 数组');
|
|
21
|
+
|
|
22
|
+
// 验证 admin 工厂获取实例
|
|
23
|
+
const inst = mod.monitorAdmin('test_scope', { listen: [] });
|
|
24
|
+
assert.ok(inst instanceof mod.Monitor, 'monitorAdmin 返回的实例应为 Monitor 类型');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
runTest().then(() => {
|
|
28
|
+
console.log('mm_monitor 模块测试通过');
|
|
29
|
+
}).catch((err) => {
|
|
30
|
+
console.error(err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "health",
|
|
3
|
+
"handler": "observer",
|
|
4
|
+
"title": "健康检查观察器",
|
|
5
|
+
"description": "检查系统各组件的健康状态(内存/CPU/磁盘等)。适用场景:系统体检、运维监控或启动前置检查时。工作流提示:checks 指定待检查组件列表(默认 memory/cpu/disk),返回 healthy 判断整体是否健康、checks 为各组件检查结果。",
|
|
6
|
+
"type": "data",
|
|
7
|
+
"params": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"checks": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"description": "待检查组件列表,默认 ['memory', 'cpu', 'disk']"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"returns": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"description": "健康检查结果",
|
|
19
|
+
"properties": {
|
|
20
|
+
"healthy": {
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"description": "系统是否健康"
|
|
23
|
+
},
|
|
24
|
+
"checks": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"description": "检查结果列表"
|
|
27
|
+
},
|
|
28
|
+
"error": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "错误信息,仅调用出错时返回",
|
|
31
|
+
"properties": {
|
|
32
|
+
"code": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "错误码"
|
|
35
|
+
},
|
|
36
|
+
"message": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "错误描述"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"options": {},
|
|
45
|
+
"group": "monitor"
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "monitor",
|
|
3
|
+
"title": "系统监控",
|
|
4
|
+
"description": "监控系统CPU、内存、磁盘、网络、进程,检测端口开放和服务连通性。触发关键词:系统监控、CPU使用率、内存使用、磁盘空间、进程列表、端口检测、服务检测",
|
|
5
|
+
"category": "系统工具",
|
|
6
|
+
"group": "system",
|
|
7
|
+
"type": "function",
|
|
8
|
+
"main": "./index.js",
|
|
9
|
+
"params": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"action": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "操作类型:get_system(系统信息)/get_processes(进程列表)/check_port(端口检测)/check_service(服务检测)",
|
|
15
|
+
"enum": [
|
|
16
|
+
"get_system",
|
|
17
|
+
"get_processes",
|
|
18
|
+
"check_port",
|
|
19
|
+
"check_service"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"top": {
|
|
23
|
+
"type": "integer",
|
|
24
|
+
"description": "进程数量(action=get_processes时有效,默认20)",
|
|
25
|
+
"default": 20
|
|
26
|
+
},
|
|
27
|
+
"host": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "主机地址(action=check_port时必填)"
|
|
30
|
+
},
|
|
31
|
+
"port": {
|
|
32
|
+
"type": "integer",
|
|
33
|
+
"description": "端口号(action=check_port时必填)"
|
|
34
|
+
},
|
|
35
|
+
"url": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "服务地址(action=check_service时必填)"
|
|
38
|
+
},
|
|
39
|
+
"timeout": {
|
|
40
|
+
"type": "integer",
|
|
41
|
+
"description": "超时时间(毫秒,默认3000)",
|
|
42
|
+
"default": 3000
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": [
|
|
46
|
+
"action"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"returns": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"code": {
|
|
53
|
+
"type": "number",
|
|
54
|
+
"description": "返回代码,0表示成功",
|
|
55
|
+
"default": 0
|
|
56
|
+
},
|
|
57
|
+
"msg": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "返回消息",
|
|
60
|
+
"default": ""
|
|
61
|
+
},
|
|
62
|
+
"data": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"description": "监控结果数据"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"required": [
|
|
68
|
+
"code",
|
|
69
|
+
"msg"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"handler": "manager"
|
|
73
|
+
}
|