mm_monitor 1.0.7 → 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/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  const Monitor = require('./lib/monitor');
2
- const Admin = require('mm_corejs').Admin;
2
+ const { reg } = require('mm_admin');
3
3
 
4
- const admin = new Admin();
4
+ const admin = reg('mm_monitor');
5
5
  admin.setClass(Monitor);
6
6
 
7
7
  module.exports = {
8
8
  Monitor,
9
9
  admin,
10
10
  monitorAdmin(scope, config = {}) {
11
- if (!scope) {
12
- scope = 'os';
13
- }
14
- return admin.get(scope, config);
11
+ return admin.get(scope || 'os', config);
15
12
  }
16
- };
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'); } });
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_monitor",
3
- "version": "1.0.7",
3
+ "version": "1.2.0",
4
4
  "description": "超级美眉监听器,用于监听网站运行是否异常并给运维人员发短信、邮箱通知",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,8 +22,8 @@
22
22
  "render",
23
23
  "template"
24
24
  ],
25
- "layer": "crosscut",
26
- "group": "alerting",
25
+ "layer": "infrastructure",
26
+ "group": "observability",
27
27
  "author": "邱文武",
28
28
  "license": "ISC",
29
29
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "homepage": "https://gitee.com/qiuwenwu91/mm_monitor#readme",
33
33
  "dependencies": {
34
- "mm_corejs": "^1.1.4",
34
+ "mm_admin": "^1.1.0",
35
35
  "mm_email": "^1.1.0",
36
36
  "mm_helper": "^1.0.3",
37
37
  "mm_https": "^1.7.1",
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+ /**
3
+ * health —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/health/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_monitor').tools.health 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/health');
@@ -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,7 @@
1
+ 'use strict';
2
+ /**
3
+ * monitor —— 工具契约适配层
4
+ * 业务实现归位于模块 lib/tools/monitor/,本文件只把模块能力暴露给加载器。
5
+ * 其它模块可直接 require('mm_monitor').tools.monitor 复用。
6
+ */
7
+ module.exports = require('../../lib/tools/monitor');
@@ -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
+ }