@daemonitor/plugins 1.0.3 → 1.1.1
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/dist/index.js +9 -6
- package/dist/lib/BasePlugin.js +26 -16
- package/dist/lib/MonitoringPlugin.js +38 -22
- package/dist/lib/PluginConfigProvider.js +14 -7
- package/dist/lib/PluginLoader.js +26 -11
- package/dist/lib/PluginManager.js +78 -44
- package/dist/lib/processes.js +36 -0
- package/dist/plugins/index.js +31 -10
- package/dist/plugins/monitoring/DockerPlugin.js +170 -0
- package/dist/plugins/monitoring/LogWatchPlugin.js +212 -0
- package/dist/plugins/monitoring/LxcPlugin.js +126 -0
- package/dist/plugins/monitoring/NetScanPlugin.js +77 -0
- package/dist/plugins/monitoring/OSPlugin.js +184 -25
- package/dist/plugins/monitoring/PM2Plugin.js +71 -43
- package/dist/plugins/monitoring/WebsitePlugin.js +205 -42
- package/dist/plugins/monitoring/WordpressPlugin.js +248 -0
- package/index.ts +11 -0
- package/package.json +32 -11
- package/dist/plugins/monitoring/EwelinkPlugin.js +0 -63
- package/dist/plugins/monitoring/MongoDBPlugin.js +0 -30
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export { PluginLoader } from "./lib/PluginLoader
|
|
5
|
-
export
|
|
6
|
-
export
|
|
1
|
+
// Export core plugin utilities
|
|
2
|
+
export { PluginConfigProvider } from "./lib/PluginConfigProvider";
|
|
3
|
+
export { PluginManager } from "./lib/PluginManager";
|
|
4
|
+
export { PluginLoader } from "./lib/PluginLoader";
|
|
5
|
+
// Only export creation functions, not types
|
|
6
|
+
export { createBasePlugin } from "./lib/BasePlugin";
|
|
7
|
+
export { createMonitoringPlugin } from "./lib/MonitoringPlugin";
|
|
8
|
+
// Export the PM2 plugin factory function
|
|
9
|
+
export { createPM2Plugin } from "./plugins/monitoring/PM2Plugin";
|
package/dist/lib/BasePlugin.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import { PluginConfigProvider } from "./PluginConfigProvider
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
alias;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { PluginConfigProvider } from "./PluginConfigProvider";
|
|
2
|
+
let currentPluginIndex = 0;
|
|
3
|
+
export function createBasePlugin(alias, name, description, setupFn, monitorFn, refreshFn, teardownFn) {
|
|
4
|
+
const config = PluginConfigProvider.get(alias);
|
|
5
|
+
const plugin = {
|
|
6
|
+
alias,
|
|
7
|
+
name,
|
|
8
|
+
description,
|
|
9
|
+
config,
|
|
10
|
+
setup: async function () {
|
|
11
|
+
return setupFn(this);
|
|
12
|
+
},
|
|
13
|
+
monitor: async function () {
|
|
14
|
+
return monitorFn(this);
|
|
15
|
+
},
|
|
16
|
+
refresh: async function () {
|
|
17
|
+
return refreshFn(this);
|
|
18
|
+
},
|
|
19
|
+
teardown: async function () {
|
|
20
|
+
return teardownFn(this);
|
|
21
|
+
},
|
|
22
|
+
getName: function () {
|
|
23
|
+
return this.name;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return plugin;
|
|
16
27
|
}
|
|
17
|
-
export { BasePlugin };
|
|
@@ -1,24 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
uniqueId
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { PluginManager } from "./PluginManager";
|
|
2
|
+
let monitoringPluginIndex = 0;
|
|
3
|
+
export function createMonitoringPlugin(alias, name, description, setupFn, monitorFn, refreshFn, teardownFn) {
|
|
4
|
+
const config = PluginManager.getConfig ? PluginManager.getConfig(alias) : {};
|
|
5
|
+
const pluginIndex = monitoringPluginIndex++;
|
|
6
|
+
const instanceName = config?.name || name;
|
|
7
|
+
const uniqueId = config?.uniqueId || `${alias}-${pluginIndex}`;
|
|
8
|
+
const plugin = {
|
|
9
|
+
alias,
|
|
10
|
+
name,
|
|
11
|
+
description,
|
|
12
|
+
config,
|
|
13
|
+
instanceName,
|
|
14
|
+
uniqueId,
|
|
15
|
+
pluginIndex,
|
|
16
|
+
setup: async function () {
|
|
17
|
+
return setupFn(this);
|
|
18
|
+
},
|
|
19
|
+
monitor: async function () {
|
|
20
|
+
return monitorFn(this);
|
|
21
|
+
},
|
|
22
|
+
refresh: async function () {
|
|
23
|
+
return refreshFn(this);
|
|
24
|
+
},
|
|
25
|
+
teardown: async function () {
|
|
26
|
+
return teardownFn(this);
|
|
27
|
+
},
|
|
28
|
+
getName: function () {
|
|
29
|
+
return this.name;
|
|
30
|
+
},
|
|
31
|
+
send: async function (data, unique_id) {
|
|
32
|
+
// Access API_CONNECTIONS from PluginManager
|
|
33
|
+
const connections = PluginManager.API_CONNECTIONS || [];
|
|
34
|
+
for (const apiConnection of connections) {
|
|
35
|
+
await apiConnection.sendData({ name: this.instanceName, ...data }, this.name, unique_id || this.uniqueId);
|
|
36
|
+
}
|
|
18
37
|
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return this.name;
|
|
22
|
-
}
|
|
38
|
+
};
|
|
39
|
+
return plugin;
|
|
23
40
|
}
|
|
24
|
-
export { MonitoringPlugin };
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
class PluginConfigProvider {
|
|
1
|
+
export class PluginConfigProvider {
|
|
2
2
|
static config = {};
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Load the full client config (the parsed client.config.json). Per-plugin
|
|
5
|
+
* sections are keyed by alias (e.g. config.docker, config.pm2). Without this
|
|
6
|
+
* the per-alias `get()` below always returns null and plugins silently fall
|
|
7
|
+
* back to their hardcoded defaults.
|
|
8
|
+
*/
|
|
9
|
+
static loadAll(config) {
|
|
10
|
+
PluginConfigProvider.config = config || {};
|
|
11
|
+
}
|
|
12
|
+
/** Synchronous lookup of a plugin's config section by alias. */
|
|
13
|
+
static get(key) {
|
|
14
|
+
const section = PluginConfigProvider.config?.[key];
|
|
15
|
+
return section == null ? null : section;
|
|
8
16
|
}
|
|
9
17
|
}
|
|
10
|
-
export { PluginConfigProvider };
|
package/dist/lib/PluginLoader.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
1
|
+
import pluginsFactory from "../plugins/index";
|
|
2
|
+
export const PluginLoader = {
|
|
3
|
+
// Load all plugins using the factories
|
|
4
|
+
loadPlugins: async function () {
|
|
5
|
+
try {
|
|
6
|
+
const plugins = [];
|
|
7
|
+
// Create plugin instances using the factory functions
|
|
8
|
+
const pluginFactories = pluginsFactory;
|
|
9
|
+
for (const pluginName of Object.keys(pluginFactories)) {
|
|
10
|
+
const createPlugin = pluginFactories[pluginName];
|
|
11
|
+
if (typeof createPlugin === 'function') {
|
|
12
|
+
try {
|
|
13
|
+
const plugin = await createPlugin();
|
|
14
|
+
plugins.push(plugin);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
console.error(`Error creating plugin ${pluginName}:`, err);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return plugins;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error("Error loading plugins:", error);
|
|
25
|
+
return [];
|
|
10
26
|
}
|
|
11
|
-
return plugins;
|
|
12
27
|
}
|
|
13
|
-
}
|
|
28
|
+
};
|
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
import { PluginLoader } from "./PluginLoader
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { PluginLoader } from "./PluginLoader";
|
|
2
|
+
import { PluginConfigProvider } from "./PluginConfigProvider";
|
|
3
|
+
// Singleton state
|
|
4
|
+
const state = {
|
|
5
|
+
availablePlugins: [],
|
|
6
|
+
activePlugins: [],
|
|
7
|
+
configuredPlugins: []
|
|
8
|
+
};
|
|
9
|
+
// Export as namespace instead of class
|
|
10
|
+
export const PluginManager = {
|
|
11
|
+
// Global connections array
|
|
12
|
+
API_CONNECTIONS: [],
|
|
13
|
+
// Initialize the manager. Accepts either the legacy string[] of plugin
|
|
14
|
+
// aliases, or the full client config object ({ plugins: [...], docker: {...}, ... }).
|
|
15
|
+
// Passing the full object also populates PluginConfigProvider so plugins
|
|
16
|
+
// receive their per-alias config section (dockerBin, refreshInterval, etc.).
|
|
17
|
+
initialize: async function (config) {
|
|
18
|
+
let configuredPlugins;
|
|
19
|
+
if (Array.isArray(config)) {
|
|
20
|
+
configuredPlugins = config;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
configuredPlugins = config?.plugins || [];
|
|
24
|
+
PluginConfigProvider.loadAll(config);
|
|
25
|
+
}
|
|
26
|
+
state.configuredPlugins = configuredPlugins.map((p) => String(p).toLowerCase());
|
|
27
|
+
await this.loadPlugins();
|
|
28
|
+
await this.setupAll();
|
|
29
|
+
return;
|
|
30
|
+
},
|
|
31
|
+
// Load all available plugins
|
|
32
|
+
loadPlugins: async function () {
|
|
33
|
+
try {
|
|
34
|
+
const plugins = await PluginLoader.loadPlugins();
|
|
17
35
|
if (!plugins) {
|
|
18
36
|
throw new Error("Failed to load plugins.");
|
|
19
37
|
}
|
|
@@ -23,47 +41,63 @@ class PluginManager {
|
|
|
23
41
|
else {
|
|
24
42
|
console.log(`Loaded ${plugins.length} plugins.`);
|
|
25
43
|
}
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
state.availablePlugins = plugins;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error("Error loading plugins:", error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
// Add a connector for plugins to use
|
|
52
|
+
addConnector: function (apiConnection) {
|
|
53
|
+
this.API_CONNECTIONS.push(apiConnection);
|
|
54
|
+
},
|
|
55
|
+
// Get connections for plugins
|
|
56
|
+
getConnections: function () {
|
|
57
|
+
return this.API_CONNECTIONS;
|
|
58
|
+
},
|
|
59
|
+
// Get config for a plugin by alias
|
|
60
|
+
getConfig: function (alias) {
|
|
61
|
+
return PluginConfigProvider.get(alias);
|
|
62
|
+
},
|
|
63
|
+
// Set up all the configured plugins
|
|
64
|
+
setupAll: async function () {
|
|
33
65
|
console.log("Setting up plugins...");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!this.configuredPlugins)
|
|
66
|
+
console.log("Configured plugins:", state.configuredPlugins.join(", "));
|
|
67
|
+
if (!state.configuredPlugins || state.configuredPlugins.length === 0) {
|
|
37
68
|
throw new Error("No plugins configured. Set up config.json first.");
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
}
|
|
70
|
+
state.activePlugins = state.availablePlugins.filter(plugin => {
|
|
71
|
+
return state.configuredPlugins.includes(plugin.getName().toLowerCase());
|
|
40
72
|
});
|
|
41
|
-
if (
|
|
73
|
+
if (state.activePlugins.length === 0) {
|
|
42
74
|
throw new Error("No plugins to set up.");
|
|
43
|
-
|
|
75
|
+
}
|
|
76
|
+
for (const plugin of state.activePlugins) {
|
|
44
77
|
await plugin.setup();
|
|
45
78
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
79
|
+
},
|
|
80
|
+
// Start monitoring with all plugins
|
|
81
|
+
monitorAll: async function () {
|
|
48
82
|
const results = [];
|
|
49
|
-
if (!
|
|
83
|
+
if (!state.availablePlugins || state.availablePlugins.length === 0) {
|
|
50
84
|
throw new Error("Plugins not loaded yet! Call loadPlugins() first.");
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!PluginManager.API_CONNECTIONS || PluginManager.API_CONNECTIONS.length === 0)
|
|
85
|
+
}
|
|
86
|
+
if (this.API_CONNECTIONS.length === 0) {
|
|
54
87
|
throw new Error("No connections loaded yet! Call addConnector() first.");
|
|
55
|
-
|
|
56
|
-
|
|
88
|
+
}
|
|
89
|
+
console.log(`Monitoring ${state.activePlugins.length} plugins...`);
|
|
90
|
+
for (const plugin of state.activePlugins) {
|
|
57
91
|
console.log(` + ${plugin.getName()}`);
|
|
58
|
-
const data = plugin.monitor();
|
|
92
|
+
const data = await plugin.monitor();
|
|
59
93
|
results.push(data);
|
|
60
94
|
}
|
|
61
95
|
return results;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
},
|
|
97
|
+
// Tear down all plugins
|
|
98
|
+
teardownAll: async function () {
|
|
99
|
+
for (const plugin of state.activePlugins) {
|
|
65
100
|
await plugin.teardown();
|
|
66
101
|
}
|
|
67
102
|
}
|
|
68
|
-
}
|
|
69
|
-
export { PluginManager };
|
|
103
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Aggregate named service processes (nginx, php-fpm, db, …) from `ps` output.
|
|
2
|
+
// Used for host processes (OSPlugin) and per-container processes (LxcPlugin via
|
|
3
|
+
// `lxc exec … ps`). MEM (rss) is exact; CPU is `ps` %cpu, which is the average
|
|
4
|
+
// over each process's lifetime — a rough indicator for long-running daemons.
|
|
5
|
+
// The command whose output parseProcesses() expects (headerless: comm rss pcpu).
|
|
6
|
+
export const PS_CMD = "ps -eo comm=,rss=,pcpu=";
|
|
7
|
+
const WATCH = [
|
|
8
|
+
{ key: "nginx", test: (c) => c.startsWith("nginx") },
|
|
9
|
+
{ key: "php-fpm", test: (c) => /^php-?fpm/i.test(c) },
|
|
10
|
+
{ key: "mysql", test: (c) => c.startsWith("mysqld") || c.startsWith("mariadbd") },
|
|
11
|
+
{ key: "postgres", test: (c) => c.startsWith("postgres") },
|
|
12
|
+
{ key: "redis", test: (c) => c.startsWith("redis-server") || c.startsWith("redis") },
|
|
13
|
+
{ key: "node", test: (c) => c.startsWith("node") },
|
|
14
|
+
];
|
|
15
|
+
export function parseProcesses(out) {
|
|
16
|
+
const agg = new Map();
|
|
17
|
+
for (const line of String(out || "").split("\n")) {
|
|
18
|
+
// Last two whitespace-separated tokens are rss + pcpu; the rest is comm
|
|
19
|
+
// (which may itself contain spaces/slashes, e.g. node process titles).
|
|
20
|
+
const m = line.trim().match(/^(.+?)\s+(\d+)\s+([\d.]+)$/);
|
|
21
|
+
if (!m)
|
|
22
|
+
continue;
|
|
23
|
+
const comm = m[1].trim();
|
|
24
|
+
const w = WATCH.find((x) => x.test(comm));
|
|
25
|
+
if (!w)
|
|
26
|
+
continue;
|
|
27
|
+
const cur = agg.get(w.key) || { name: w.key, count: 0, cpu: 0, mem: 0 };
|
|
28
|
+
cur.count++;
|
|
29
|
+
cur.cpu += Number(m[3]) || 0;
|
|
30
|
+
cur.mem += (Number(m[2]) || 0) * 1024; // rss KiB -> bytes
|
|
31
|
+
agg.set(w.key, cur);
|
|
32
|
+
}
|
|
33
|
+
return Array.from(agg.values())
|
|
34
|
+
.map((p) => ({ ...p, cpu: Math.round(p.cpu * 10) / 10 }))
|
|
35
|
+
.sort((a, b) => b.mem - a.mem);
|
|
36
|
+
}
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
// Import plugin factory functions
|
|
2
|
+
import { createPM2Plugin } from "./monitoring/PM2Plugin";
|
|
3
|
+
import { createDockerPlugin } from "./monitoring/DockerPlugin";
|
|
4
|
+
import { createLxcPlugin } from "./monitoring/LxcPlugin";
|
|
5
|
+
import { createOSPlugin } from "./monitoring/OSPlugin";
|
|
6
|
+
import { createWebsitePlugin } from "./monitoring/WebsitePlugin";
|
|
7
|
+
import { createWordpressPlugin } from "./monitoring/WordpressPlugin";
|
|
8
|
+
import { createLogWatchPlugin } from "./monitoring/LogWatchPlugin";
|
|
9
|
+
import { createNetScanPlugin } from "./monitoring/NetScanPlugin";
|
|
10
|
+
// In future, implement other plugin factory functions using the same pattern
|
|
11
|
+
// import { createOSPlugin } from "./monitoring/OSPlugin"
|
|
12
|
+
// import { createMongoDBPlugin } from "./monitoring/MongoDBPlugin"
|
|
13
|
+
// import { createWebsitePlugin } from "./monitoring/WebsitePlugin"
|
|
14
|
+
// import { createCloudflarePlugin } from "./monitoring/CloudflarePlugin"
|
|
15
|
+
// import { createEwelinkPlugin } from "./monitoring/EwelinkPlugin"
|
|
16
|
+
// Export factory functions to create plugin instances.
|
|
17
|
+
// (Keys are display-only; the manager activates a plugin when its getName()
|
|
18
|
+
// lowercased matches an alias in the config `plugins` array.)
|
|
6
19
|
export default {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
PM2Plugin: createPM2Plugin,
|
|
21
|
+
Docker: createDockerPlugin,
|
|
22
|
+
Lxc: createLxcPlugin,
|
|
23
|
+
OS: createOSPlugin,
|
|
24
|
+
Website: createWebsitePlugin,
|
|
25
|
+
Wordpress: createWordpressPlugin,
|
|
26
|
+
Logwatch: createLogWatchPlugin,
|
|
27
|
+
Netscan: createNetScanPlugin,
|
|
28
|
+
// CloudflarePlugin: createCloudflarePlugin,
|
|
29
|
+
// EwelinkPlugin: createEwelinkPlugin,
|
|
30
|
+
// MongoDBPlugin: createMongoDBPlugin,
|
|
31
|
+
// OSPlugin: createOSPlugin,
|
|
32
|
+
// WebsitePlugin: createWebsitePlugin,
|
|
12
33
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { hostname } from "os";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
const EXEC_OPTS = { maxBuffer: 32 * 1024 * 1024 }; // containers.length can be large
|
|
7
|
+
const HEALTH_RE = /\((healthy|unhealthy|health: starting|starting)\)/i;
|
|
8
|
+
/** Convert docker's "83.86MiB" / "1.952GiB" memory strings to bytes. */
|
|
9
|
+
function memToBytes(value) {
|
|
10
|
+
if (!value)
|
|
11
|
+
return 0;
|
|
12
|
+
const unit = value.replace(/[0-9.\s]/g, "").toUpperCase();
|
|
13
|
+
const num = parseFloat(value.replace(/[^0-9.]/g, "")) || 0;
|
|
14
|
+
const mult = {
|
|
15
|
+
B: 1, KB: 1e3, KIB: 1024, MB: 1e6, MIB: 1024 ** 2,
|
|
16
|
+
GB: 1e9, GIB: 1024 ** 3, TB: 1e12, TIB: 1024 ** 4,
|
|
17
|
+
};
|
|
18
|
+
return Math.round(num * (mult[unit] ?? 1));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Collect all containers (including stopped) in a small, fixed number of
|
|
22
|
+
* exec calls regardless of how many containers exist:
|
|
23
|
+
* 1. `docker ps -a` — names, image, state, status, compose labels
|
|
24
|
+
* 2. `docker stats` — live CPU / memory for running containers (one batch)
|
|
25
|
+
* 3. `docker inspect` — restart count, health, exit code (one batch)
|
|
26
|
+
*/
|
|
27
|
+
async function collectContainers(bin) {
|
|
28
|
+
const psFmt = '{{.ID}}|{{.Names}}|{{.Image}}|{{.State}}|{{.Status}}|{{.Label "com.docker.compose.project"}}|{{.Label "com.docker.compose.service"}}';
|
|
29
|
+
const { stdout: psOut } = await execAsync(`${bin} ps -a --format '${psFmt}'`, EXEC_OPTS);
|
|
30
|
+
const containers = [];
|
|
31
|
+
const byId = new Map();
|
|
32
|
+
for (const line of psOut.split("\n")) {
|
|
33
|
+
if (!line.trim())
|
|
34
|
+
continue;
|
|
35
|
+
const [id, name, image, state, status, project, service] = line.split("|");
|
|
36
|
+
const running = (state || "").toLowerCase() === "running";
|
|
37
|
+
const healthMatch = HEALTH_RE.exec(status || "");
|
|
38
|
+
const c = {
|
|
39
|
+
id,
|
|
40
|
+
name,
|
|
41
|
+
service: service || "",
|
|
42
|
+
image,
|
|
43
|
+
state: state || "",
|
|
44
|
+
status: status || "",
|
|
45
|
+
running,
|
|
46
|
+
health: healthMatch ? healthMatch[1].replace(/^health:\s*/i, "").toLowerCase() : "",
|
|
47
|
+
cpu: 0, mem: 0, memLimit: 0, memPercent: 0, restarts: 0, exitCode: 0,
|
|
48
|
+
};
|
|
49
|
+
c.__project = project || "";
|
|
50
|
+
containers.push(c);
|
|
51
|
+
byId.set(id, c);
|
|
52
|
+
}
|
|
53
|
+
if (!containers.length)
|
|
54
|
+
return containers;
|
|
55
|
+
// 2. live stats for running containers (one batched call, best-effort)
|
|
56
|
+
try {
|
|
57
|
+
const { stdout: statsOut } = await execAsync(`${bin} stats --no-stream --format '{{.ID}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}'`, EXEC_OPTS);
|
|
58
|
+
for (const line of statsOut.split("\n")) {
|
|
59
|
+
if (!line.trim())
|
|
60
|
+
continue;
|
|
61
|
+
const [id, cpuPerc, memUsage, memPerc] = line.split("|");
|
|
62
|
+
const c = byId.get(id);
|
|
63
|
+
if (!c)
|
|
64
|
+
continue;
|
|
65
|
+
c.cpu = parseFloat((cpuPerc || "").replace("%", "")) || 0;
|
|
66
|
+
const [used, limit] = (memUsage || "").split("/").map((p) => p.trim());
|
|
67
|
+
c.mem = memToBytes(used);
|
|
68
|
+
c.memLimit = memToBytes(limit);
|
|
69
|
+
c.memPercent = parseFloat((memPerc || "").replace("%", "")) || 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error("docker: stats collection failed:", err.message);
|
|
74
|
+
}
|
|
75
|
+
// 3. restart count + exit code (one batched inspect, best-effort).
|
|
76
|
+
// NOTE: we deliberately do NOT read .State.Health here — on some engines the
|
|
77
|
+
// template errors with `map has no entry for key "Health"` for containers
|
|
78
|
+
// without a healthcheck, which would fail the whole batch. Health is parsed
|
|
79
|
+
// from the ps `Status` string ("(healthy)" / "(unhealthy)") instead.
|
|
80
|
+
try {
|
|
81
|
+
const ids = containers.map((c) => c.id).join(" ");
|
|
82
|
+
const inspectFmt = '{{.Id}}|{{.RestartCount}}|{{.State.ExitCode}}';
|
|
83
|
+
const { stdout: inspOut } = await execAsync(`${bin} inspect --format '${inspectFmt}' ${ids}`, EXEC_OPTS);
|
|
84
|
+
for (const line of inspOut.split("\n")) {
|
|
85
|
+
if (!line.trim())
|
|
86
|
+
continue;
|
|
87
|
+
const [fullId, restarts, exitCode] = line.split("|");
|
|
88
|
+
const c = byId.get(fullId.slice(0, 12));
|
|
89
|
+
if (!c)
|
|
90
|
+
continue;
|
|
91
|
+
c.restarts = parseInt(restarts, 10) || 0;
|
|
92
|
+
c.exitCode = parseInt(exitCode, 10) || 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.error("docker: inspect collection failed:", err.message);
|
|
97
|
+
}
|
|
98
|
+
return containers;
|
|
99
|
+
}
|
|
100
|
+
export function createDockerPlugin() {
|
|
101
|
+
let refreshTimer = null;
|
|
102
|
+
let available = false;
|
|
103
|
+
let dockerBin = "docker";
|
|
104
|
+
const setupFn = async (plugin) => {
|
|
105
|
+
dockerBin = plugin.config?.dockerBin || plugin.config?.bin || "docker";
|
|
106
|
+
try {
|
|
107
|
+
await execAsync(`${dockerBin} version --format '{{.Server.Version}}'`, EXEC_OPTS);
|
|
108
|
+
available = true;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// Don't throw: a missing docker engine shouldn't take down the whole client.
|
|
112
|
+
available = false;
|
|
113
|
+
console.error(`docker: engine not available via "${dockerBin}" — plugin idle:`, err.message);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const refreshFn = async (plugin) => {
|
|
117
|
+
if (!available)
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const all = await collectContainers(dockerBin);
|
|
121
|
+
// Optional filtering by compose project.
|
|
122
|
+
const only = plugin.config?.projects || [];
|
|
123
|
+
const exclude = plugin.config?.exclude || [];
|
|
124
|
+
// Group by compose project (standalone containers group under their own name).
|
|
125
|
+
const groups = new Map();
|
|
126
|
+
for (const c of all) {
|
|
127
|
+
const project = c.__project || c.name;
|
|
128
|
+
delete c.__project;
|
|
129
|
+
if (only.length && !only.includes(project))
|
|
130
|
+
continue;
|
|
131
|
+
if (exclude.includes(project))
|
|
132
|
+
continue;
|
|
133
|
+
const arr = groups.get(project) || [];
|
|
134
|
+
arr.push(c);
|
|
135
|
+
groups.set(project, arr);
|
|
136
|
+
}
|
|
137
|
+
const host = hostname();
|
|
138
|
+
for (const [project, containers] of groups) {
|
|
139
|
+
const running = containers.filter((c) => c.running).length;
|
|
140
|
+
const payload = {
|
|
141
|
+
name: project,
|
|
142
|
+
engine: "docker",
|
|
143
|
+
host,
|
|
144
|
+
project,
|
|
145
|
+
running,
|
|
146
|
+
total: containers.length,
|
|
147
|
+
containers,
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
};
|
|
150
|
+
await plugin.send(payload, `docker-${project}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error("docker: refresh error:", err.message);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const monitorFn = async (plugin) => {
|
|
158
|
+
if (!available)
|
|
159
|
+
return;
|
|
160
|
+
await refreshFn(plugin);
|
|
161
|
+
refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 60000);
|
|
162
|
+
};
|
|
163
|
+
const teardownFn = async () => {
|
|
164
|
+
if (refreshTimer)
|
|
165
|
+
clearInterval(refreshTimer);
|
|
166
|
+
refreshTimer = null;
|
|
167
|
+
};
|
|
168
|
+
return createMonitoringPlugin("docker", "docker", "Docker container monitoring plugin", setupFn, monitorFn, refreshFn, teardownFn);
|
|
169
|
+
}
|
|
170
|
+
export default createDockerPlugin;
|