@daemonitor/plugins 1.0.3 → 1.1.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/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/nuxt.config.js +1 -0
- package/dist/plugins/index.js +25 -10
- package/dist/plugins/monitoring/CloudflarePlugin.js +84 -0
- package/dist/plugins/monitoring/DockerPlugin.js +170 -0
- package/dist/plugins/monitoring/EwelinkPlugin.js +1 -1
- package/dist/plugins/monitoring/LxcPlugin.js +126 -0
- package/dist/plugins/monitoring/OSPlugin.js +55 -28
- package/dist/plugins/monitoring/PM2Plugin.js +71 -43
- package/dist/plugins/monitoring/WebsitePlugin.js +71 -45
- package/index.ts +11 -0
- package/package.json +28 -10
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 @@
|
|
|
1
|
+
export default defineNuxtConfig({});
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
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
|
+
// In future, implement other plugin factory functions using the same pattern
|
|
8
|
+
// import { createOSPlugin } from "./monitoring/OSPlugin"
|
|
9
|
+
// import { createMongoDBPlugin } from "./monitoring/MongoDBPlugin"
|
|
10
|
+
// import { createWebsitePlugin } from "./monitoring/WebsitePlugin"
|
|
11
|
+
// import { createCloudflarePlugin } from "./monitoring/CloudflarePlugin"
|
|
12
|
+
// import { createEwelinkPlugin } from "./monitoring/EwelinkPlugin"
|
|
13
|
+
// Export factory functions to create plugin instances.
|
|
14
|
+
// (Keys are display-only; the manager activates a plugin when its getName()
|
|
15
|
+
// lowercased matches an alias in the config `plugins` array.)
|
|
6
16
|
export default {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
PM2Plugin: createPM2Plugin,
|
|
18
|
+
Docker: createDockerPlugin,
|
|
19
|
+
Lxc: createLxcPlugin,
|
|
20
|
+
OS: createOSPlugin,
|
|
21
|
+
Website: createWebsitePlugin,
|
|
22
|
+
// CloudflarePlugin: createCloudflarePlugin,
|
|
23
|
+
// EwelinkPlugin: createEwelinkPlugin,
|
|
24
|
+
// MongoDBPlugin: createMongoDBPlugin,
|
|
25
|
+
// OSPlugin: createOSPlugin,
|
|
26
|
+
// WebsitePlugin: createWebsitePlugin,
|
|
12
27
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { MonitoringPlugin } from "../../lib/MonitoringPlugin.js";
|
|
2
|
+
/**
|
|
3
|
+
* CloudflarePlugin - Receives push updates from Cloudflare Workers
|
|
4
|
+
*
|
|
5
|
+
* This plugin receives data directly from a client's Cloudflare Worker
|
|
6
|
+
* rather than polling for it. The plugin sets up a monitoring endpoint,
|
|
7
|
+
* and the Cloudflare Worker pushes data to this endpoint on a schedule.
|
|
8
|
+
*/
|
|
9
|
+
export class CloudflarePlugin extends MonitoringPlugin {
|
|
10
|
+
lastReportedData = null;
|
|
11
|
+
lastReportTime = 0;
|
|
12
|
+
reportingKey;
|
|
13
|
+
maxDataAge;
|
|
14
|
+
constructor() {
|
|
15
|
+
super("cloudflare", "Cloudflare", "Cloudflare Deployment Monitoring Plugin (Push Mode)");
|
|
16
|
+
this.reportingKey = this.config.reportingKey;
|
|
17
|
+
this.maxDataAge = this.config.maxDataAge || 5 * 60 * 1000; // Default 5 minutes
|
|
18
|
+
}
|
|
19
|
+
async setup() {
|
|
20
|
+
if (!this.reportingKey) {
|
|
21
|
+
console.error("Cloudflare reporting key not provided");
|
|
22
|
+
return Promise.reject("Reporting key not provided");
|
|
23
|
+
}
|
|
24
|
+
// Register this instance to receive push updates
|
|
25
|
+
try {
|
|
26
|
+
// Normally this would make an API call to register this plugin instance
|
|
27
|
+
// to receive push updates from Cloudflare Workers with this reporting key
|
|
28
|
+
console.log(`Cloudflare plugin ready to receive push updates for key: ${this.reportingKey}`);
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("Failed to register for Cloudflare push updates:", error);
|
|
33
|
+
return Promise.reject("Registration failed");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* This method is called when data is pushed from a Cloudflare Worker
|
|
38
|
+
* It would typically be called by a webhook or socket connection
|
|
39
|
+
*/
|
|
40
|
+
async receiveUpdate(data, key) {
|
|
41
|
+
// Verify the reporting key matches
|
|
42
|
+
if (key !== this.reportingKey) {
|
|
43
|
+
console.error("Invalid reporting key for Cloudflare update");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.lastReportedData = {
|
|
47
|
+
...data,
|
|
48
|
+
receivedAt: Date.now()
|
|
49
|
+
};
|
|
50
|
+
this.lastReportTime = Date.now();
|
|
51
|
+
// Forward the data to the monitoring system
|
|
52
|
+
await this.send({
|
|
53
|
+
...data,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
status: "ok"
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// This method checks if data is still fresh
|
|
59
|
+
async refresh() {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const dataAge = now - this.lastReportTime;
|
|
62
|
+
// If we have data and it's still fresh, do nothing
|
|
63
|
+
if (this.lastReportedData && dataAge < this.maxDataAge) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// If data is stale or doesn't exist, report error status
|
|
67
|
+
await this.send({
|
|
68
|
+
timestamp: now,
|
|
69
|
+
error: `No data received in the last ${dataAge / 60000} minutes`,
|
|
70
|
+
status: "error",
|
|
71
|
+
lastReportTime: this.lastReportTime
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async monitor() {
|
|
75
|
+
// Set up interval just to check for stale data
|
|
76
|
+
this.refreshTimer = setInterval(this.refresh.bind(this), this.config.refreshInterval || 60000 // Default to 1 minute
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
async teardown() {
|
|
80
|
+
// Unregister from receiving push updates
|
|
81
|
+
clearInterval(this.refreshTimer);
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ewelink from "ewelink-api";
|
|
2
2
|
import * as dotenv from "dotenv";
|
|
3
|
-
import { MonitoringPlugin } from "
|
|
3
|
+
import { MonitoringPlugin } from "~/lib/MonitoringPlugin";
|
|
4
4
|
dotenv.config();
|
|
5
5
|
export class EwelinkPlugin extends MonitoringPlugin {
|
|
6
6
|
connection;
|
|
@@ -0,0 +1,126 @@
|
|
|
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 };
|
|
7
|
+
/** Parse LXD memory-limit strings like "1GB", "512MiB", "2GiB" to bytes. */
|
|
8
|
+
function limitToBytes(value) {
|
|
9
|
+
if (!value)
|
|
10
|
+
return 0;
|
|
11
|
+
const unit = String(value).replace(/[0-9.\s]/g, "").toUpperCase();
|
|
12
|
+
const num = parseFloat(String(value).replace(/[^0-9.]/g, "")) || 0;
|
|
13
|
+
const mult = {
|
|
14
|
+
"": 1, B: 1, KB: 1e3, KIB: 1024, MB: 1e6, MIB: 1024 ** 2,
|
|
15
|
+
GB: 1e9, GIB: 1024 ** 3, TB: 1e12, TIB: 1024 ** 4,
|
|
16
|
+
};
|
|
17
|
+
return Math.round(num * (mult[unit] ?? 1));
|
|
18
|
+
}
|
|
19
|
+
export function createLxcPlugin() {
|
|
20
|
+
let refreshTimer = null;
|
|
21
|
+
let available = false;
|
|
22
|
+
let lxcBin = "lxc";
|
|
23
|
+
// cumulative CPU usage (ns) + wall clock (ms) per instance, for %-of-core calc
|
|
24
|
+
const prevCpu = new Map();
|
|
25
|
+
const setupFn = async (plugin) => {
|
|
26
|
+
lxcBin = plugin.config?.lxcBin || plugin.config?.bin || "lxc";
|
|
27
|
+
try {
|
|
28
|
+
await execAsync(`${lxcBin} version`, EXEC_OPTS);
|
|
29
|
+
available = true;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
available = false;
|
|
33
|
+
console.error(`lxc: "${lxcBin}" not available — plugin idle:`, err.message);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const refreshFn = async (plugin) => {
|
|
37
|
+
if (!available)
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execAsync(`${lxcBin} list --format json`, EXEC_OPTS);
|
|
41
|
+
const instances = JSON.parse(stdout || "[]");
|
|
42
|
+
const only = plugin.config?.instances || [];
|
|
43
|
+
const exclude = plugin.config?.exclude || [];
|
|
44
|
+
const host = hostname();
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const groups = new Map();
|
|
47
|
+
for (const inst of instances) {
|
|
48
|
+
const name = inst.name;
|
|
49
|
+
if (!name)
|
|
50
|
+
continue;
|
|
51
|
+
if (only.length && !only.includes(name))
|
|
52
|
+
continue;
|
|
53
|
+
if (exclude.includes(name))
|
|
54
|
+
continue;
|
|
55
|
+
const running = String(inst.status || "").toLowerCase() === "running";
|
|
56
|
+
const st = inst.state || {};
|
|
57
|
+
// CPU %: delta of cumulative usage(ns) over wall time -> % of one core
|
|
58
|
+
const usage = Number(st.cpu?.usage) || 0;
|
|
59
|
+
let cpu = 0;
|
|
60
|
+
const p = prevCpu.get(name);
|
|
61
|
+
if (running && p && now > p.t) {
|
|
62
|
+
const dUsage = usage - p.usage;
|
|
63
|
+
const dWallNs = (now - p.t) * 1e6;
|
|
64
|
+
if (dWallNs > 0 && dUsage >= 0)
|
|
65
|
+
cpu = Math.round((dUsage / dWallNs) * 1000) / 10;
|
|
66
|
+
}
|
|
67
|
+
prevCpu.set(name, { usage, t: now });
|
|
68
|
+
const mem = Number(st.memory?.usage) || 0;
|
|
69
|
+
const memLimit = limitToBytes(inst.config?.["limits.memory"] || inst.expanded_config?.["limits.memory"]);
|
|
70
|
+
const memPercent = memLimit ? Math.round((mem / memLimit) * 1000) / 10 : 0;
|
|
71
|
+
const container = {
|
|
72
|
+
id: name,
|
|
73
|
+
name,
|
|
74
|
+
service: "",
|
|
75
|
+
image: inst.config?.["image.description"] || inst.type || "lxc",
|
|
76
|
+
state: String(inst.status || "").toLowerCase(),
|
|
77
|
+
status: inst.status || "",
|
|
78
|
+
running,
|
|
79
|
+
health: "",
|
|
80
|
+
cpu,
|
|
81
|
+
mem,
|
|
82
|
+
memLimit,
|
|
83
|
+
memPercent,
|
|
84
|
+
restarts: 0,
|
|
85
|
+
exitCode: 0,
|
|
86
|
+
};
|
|
87
|
+
// Grouping: a user.daemonitor.group config key, else the host.
|
|
88
|
+
const group = inst.config?.["user.daemonitor.group"] ||
|
|
89
|
+
inst.expanded_config?.["user.daemonitor.group"] || host;
|
|
90
|
+
const arr = groups.get(group) || [];
|
|
91
|
+
arr.push(container);
|
|
92
|
+
groups.set(group, arr);
|
|
93
|
+
}
|
|
94
|
+
for (const [project, containers] of groups) {
|
|
95
|
+
const running = containers.filter((c) => c.running).length;
|
|
96
|
+
const payload = {
|
|
97
|
+
name: project,
|
|
98
|
+
engine: "lxc",
|
|
99
|
+
host,
|
|
100
|
+
project,
|
|
101
|
+
running,
|
|
102
|
+
total: containers.length,
|
|
103
|
+
containers,
|
|
104
|
+
timestamp: now,
|
|
105
|
+
};
|
|
106
|
+
await plugin.send(payload, `lxc-${project}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
console.error("lxc: refresh error:", err.message);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const monitorFn = async (plugin) => {
|
|
114
|
+
if (!available)
|
|
115
|
+
return;
|
|
116
|
+
await refreshFn(plugin);
|
|
117
|
+
refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 60000);
|
|
118
|
+
};
|
|
119
|
+
const teardownFn = async () => {
|
|
120
|
+
if (refreshTimer)
|
|
121
|
+
clearInterval(refreshTimer);
|
|
122
|
+
refreshTimer = null;
|
|
123
|
+
};
|
|
124
|
+
return createMonitoringPlugin("lxc", "lxc", "LXC / LXD / Incus container monitoring plugin", setupFn, monitorFn, refreshFn, teardownFn);
|
|
125
|
+
}
|
|
126
|
+
export default createLxcPlugin;
|
|
@@ -1,29 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
|
|
2
|
+
import { arch, cpus, freemem, hostname, loadavg, networkInterfaces, platform, release, totalmem, type, uptime, } from "os";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
// True available memory: on Linux, os.freemem() returns MemFree, which excludes
|
|
5
|
+
// reclaimable page cache and makes hosts look near-full. MemAvailable is the
|
|
6
|
+
// kernel's estimate of memory obtainable without swapping. Fall back to freemem()
|
|
7
|
+
// on non-Linux (or if /proc is unreadable).
|
|
8
|
+
function availableMem() {
|
|
9
|
+
if (platform() === "linux") {
|
|
10
|
+
try {
|
|
11
|
+
const info = readFileSync("/proc/meminfo", "utf8");
|
|
12
|
+
const m = info.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
13
|
+
if (m)
|
|
14
|
+
return parseInt(m[1], 10) * 1024;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// fall through to freemem()
|
|
18
|
+
}
|
|
10
19
|
}
|
|
11
|
-
|
|
20
|
+
return freemem();
|
|
21
|
+
}
|
|
22
|
+
// Host OS metrics: cpu (from cpus().times), memory, load, uptime, addresses.
|
|
23
|
+
// Matches the frontend `os` fleetAdapter case.
|
|
24
|
+
export function createOSPlugin() {
|
|
25
|
+
let refreshTimer = null;
|
|
26
|
+
const collect = () => {
|
|
12
27
|
const interfaces = networkInterfaces();
|
|
13
28
|
const addresses = {};
|
|
14
|
-
for (
|
|
15
|
-
if (value)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}
|
|
29
|
+
for (const [key, value] of Object.entries(interfaces)) {
|
|
30
|
+
if (!value)
|
|
31
|
+
continue;
|
|
32
|
+
const found = value.find((p) => p.family === "IPv4" && p.internal !== true);
|
|
33
|
+
if (found)
|
|
34
|
+
addresses[key] = { address: found.address, netmask: found.netmask, mac: found.mac };
|
|
22
35
|
}
|
|
23
|
-
|
|
36
|
+
return {
|
|
24
37
|
addresses,
|
|
25
38
|
totalmem: totalmem(),
|
|
26
39
|
freemem: freemem(),
|
|
40
|
+
available: availableMem(),
|
|
27
41
|
loadavg: loadavg(),
|
|
28
42
|
uptime: uptime(),
|
|
29
43
|
hostname: hostname(),
|
|
@@ -31,13 +45,26 @@ export class OSPlugin extends MonitoringPlugin {
|
|
|
31
45
|
release: release(),
|
|
32
46
|
type: type(),
|
|
33
47
|
arch: arch(),
|
|
34
|
-
cpus: cpus()
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
cpus: cpus(),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const refreshFn = async (plugin) => {
|
|
52
|
+
try {
|
|
53
|
+
await plugin.send({ name: hostname(), ...collect() }, "os");
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error("os: refresh error:", err.message);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const monitorFn = async (plugin) => {
|
|
60
|
+
await refreshFn(plugin);
|
|
61
|
+
refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 30000);
|
|
62
|
+
};
|
|
63
|
+
const teardownFn = async () => {
|
|
64
|
+
if (refreshTimer)
|
|
65
|
+
clearInterval(refreshTimer);
|
|
66
|
+
refreshTimer = null;
|
|
67
|
+
};
|
|
68
|
+
return createMonitoringPlugin("os", "os", "Host OS monitoring plugin", async () => { }, monitorFn, refreshFn, teardownFn);
|
|
43
69
|
}
|
|
70
|
+
export default createOSPlugin;
|
|
@@ -1,48 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
|
|
2
|
+
// Factory function to create PM2 plugin
|
|
3
|
+
export function createPM2Plugin() {
|
|
4
|
+
// Create plugin state
|
|
5
|
+
let pm2Instance = null;
|
|
6
|
+
let refreshTimer = null;
|
|
7
|
+
// Define setup function
|
|
8
|
+
const setupFn = async (plugin) => {
|
|
9
|
+
try {
|
|
10
|
+
// First, check if PM2 is available globally (injected by the client-test)
|
|
11
|
+
if (global.pm2Library) {
|
|
12
|
+
console.log("Using globally provided PM2 instance");
|
|
13
|
+
pm2Instance = global.pm2Library;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// Otherwise use dynamic import approach which is ESM friendly
|
|
17
|
+
try {
|
|
18
|
+
const pm2Module = await import('pm2');
|
|
19
|
+
pm2Instance = pm2Module.default || pm2Module;
|
|
20
|
+
// If import fails, try CommonJS require as fallback
|
|
21
|
+
if (!pm2Instance) {
|
|
22
|
+
try {
|
|
23
|
+
// @ts-ignore - Dynamically require pm2
|
|
24
|
+
pm2Instance = require('pm2');
|
|
25
|
+
}
|
|
26
|
+
catch (requireErr) {
|
|
27
|
+
console.error("Failed to require PM2:", requireErr);
|
|
28
|
+
throw new Error(`Neither import nor require could load pm2: ${requireErr.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error("Failed to import PM2:", err);
|
|
34
|
+
throw new Error(`PM2 import failed: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Return a promise for the PM2 connection
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
pm2Instance.connect((err) => {
|
|
11
40
|
if (err) {
|
|
12
|
-
console.error(err);
|
|
41
|
+
console.error("PM2 connection failed:", err);
|
|
13
42
|
reject(err);
|
|
14
43
|
}
|
|
15
44
|
else {
|
|
16
45
|
resolve();
|
|
17
46
|
}
|
|
18
47
|
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
console.error("PM2Plugin setup error:", e);
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
// Define refresh function
|
|
56
|
+
const refreshFn = async (plugin) => {
|
|
57
|
+
if (!pm2Instance) {
|
|
58
|
+
console.error("PM2 instance not initialized");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
27
61
|
const instanceCounts = {};
|
|
28
|
-
|
|
62
|
+
pm2Instance.list(async (err, list) => {
|
|
29
63
|
if (!err) {
|
|
30
64
|
for (const p of list) {
|
|
31
|
-
let { pid, name, pm_id, monit,
|
|
32
|
-
// exit_code,
|
|
33
|
-
// prev_restart_delay,
|
|
34
|
-
// versioning,
|
|
35
|
-
// axm_dynamic,
|
|
36
|
-
// axm_actions,
|
|
37
|
-
// merge_logs,
|
|
38
|
-
// vizion,
|
|
39
|
-
// instance_var,
|
|
40
|
-
// pmx,
|
|
41
|
-
// automation,
|
|
42
|
-
// treekill,
|
|
43
|
-
// windowsHide,
|
|
44
|
-
// kill_retry_time
|
|
45
|
-
} = p;
|
|
65
|
+
let { pid, name, pm_id, monit, } = p;
|
|
46
66
|
if (instanceCounts[name] === undefined) {
|
|
47
67
|
instanceCounts[name] = 0;
|
|
48
68
|
}
|
|
@@ -69,18 +89,26 @@ export class PM2Plugin extends MonitoringPlugin {
|
|
|
69
89
|
status,
|
|
70
90
|
pm_uptime,
|
|
71
91
|
};
|
|
72
|
-
await
|
|
92
|
+
await plugin.send(payload, instance_id);
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
95
|
else {
|
|
76
96
|
console.error(err);
|
|
77
97
|
}
|
|
78
98
|
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
};
|
|
100
|
+
// Define monitor function
|
|
101
|
+
const monitorFn = async (plugin) => {
|
|
102
|
+
refreshTimer = setInterval(() => refreshFn(plugin), plugin.config.refreshInterval || 5000);
|
|
103
|
+
};
|
|
104
|
+
// Define teardown function
|
|
105
|
+
const teardownFn = async (plugin) => {
|
|
106
|
+
if (refreshTimer) {
|
|
107
|
+
clearInterval(refreshTimer);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// Create and return the plugin
|
|
111
|
+
return createMonitoringPlugin("pm2", "PM2", "PM2 Monitoring Plugin", setupFn, monitorFn, refreshFn, teardownFn);
|
|
86
112
|
}
|
|
113
|
+
// Export a default factory function for plugin loader
|
|
114
|
+
export default createPM2Plugin;
|
|
@@ -1,46 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
1
|
+
import { createMonitoringPlugin } from "../../lib/MonitoringPlugin";
|
|
2
|
+
// HTTP uptime/content checks. Emits one client_state per endpoint, matching the
|
|
3
|
+
// frontend `web`/`website` fleetAdapter case ({ url, name, ok, status, duration }).
|
|
4
|
+
export function createWebsitePlugin() {
|
|
5
|
+
let refreshTimer = null;
|
|
6
|
+
const check = async (plugin, ep) => {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
const label = ep.name || ep.url;
|
|
9
|
+
const uid = `web-${(ep.name || ep.url).replace(/[^a-zA-Z0-9_.-]+/g, "-")}`;
|
|
10
|
+
try {
|
|
11
|
+
const ctrl = new AbortController();
|
|
12
|
+
const t = ep.timeout ? setTimeout(() => ctrl.abort(), ep.timeout) : null;
|
|
13
|
+
const res = await fetch(ep.url, { signal: ctrl.signal, redirect: "follow" });
|
|
14
|
+
if (t)
|
|
15
|
+
clearTimeout(t);
|
|
16
|
+
const body = await res.text();
|
|
17
|
+
const duration = Date.now() - start;
|
|
18
|
+
const expected = ep.expectedStrings || [];
|
|
19
|
+
const unexpected = ep.unexpectedStrings || [];
|
|
20
|
+
const hasAllExpected = expected.every((s) => body.indexOf(s) !== -1);
|
|
21
|
+
const hasAnyUnexpected = unexpected.some((s) => body.indexOf(s) !== -1);
|
|
22
|
+
const statusOk = ep.expectedStatusCode ? res.status === ep.expectedStatusCode : res.ok;
|
|
23
|
+
const ok = statusOk && hasAllExpected && !hasAnyUnexpected;
|
|
24
|
+
await plugin.send({
|
|
25
|
+
name: label,
|
|
26
|
+
url: res.url || ep.url,
|
|
27
|
+
status: res.status,
|
|
28
|
+
duration,
|
|
29
|
+
ok,
|
|
30
|
+
redirected: res.redirected,
|
|
31
|
+
hasAllExpectedStrings: hasAllExpected,
|
|
32
|
+
hasAnyUnexpectedStrings: hasAnyUnexpected,
|
|
33
|
+
}, uid);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
await plugin.send({
|
|
37
|
+
name: label,
|
|
38
|
+
url: ep.url,
|
|
39
|
+
status: 0,
|
|
40
|
+
duration: Date.now() - start,
|
|
41
|
+
ok: false,
|
|
42
|
+
error: err.message,
|
|
43
|
+
}, uid);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const endpointsOf = (plugin) => {
|
|
47
|
+
const cfg = plugin.config || {};
|
|
48
|
+
if (Array.isArray(cfg.endpoints))
|
|
49
|
+
return cfg.endpoints;
|
|
50
|
+
if (cfg.url)
|
|
51
|
+
return [{ name: cfg.name, url: cfg.url, expectedStrings: cfg.expectedStrings, unexpectedStrings: cfg.unexpectedStrings }];
|
|
52
|
+
return [];
|
|
53
|
+
};
|
|
54
|
+
const refreshFn = async (plugin) => {
|
|
55
|
+
const eps = endpointsOf(plugin);
|
|
56
|
+
for (const ep of eps) {
|
|
57
|
+
if (ep?.url)
|
|
58
|
+
await check(plugin, ep);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const monitorFn = async (plugin) => {
|
|
62
|
+
await refreshFn(plugin);
|
|
63
|
+
refreshTimer = setInterval(() => refreshFn(plugin), plugin.config?.refreshInterval || 60000);
|
|
64
|
+
};
|
|
65
|
+
const teardownFn = async () => {
|
|
66
|
+
if (refreshTimer)
|
|
67
|
+
clearInterval(refreshTimer);
|
|
68
|
+
refreshTimer = null;
|
|
69
|
+
};
|
|
70
|
+
return createMonitoringPlugin("web", "web", "Website / HTTP uptime monitoring plugin", async () => { }, monitorFn, refreshFn, teardownFn);
|
|
46
71
|
}
|
|
72
|
+
export default createWebsitePlugin;
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Export core plugin utilities
|
|
2
|
+
export { PluginConfigProvider } from "./lib/PluginConfigProvider"
|
|
3
|
+
export { PluginManager } from "./lib/PluginManager"
|
|
4
|
+
export { PluginLoader } from "./lib/PluginLoader"
|
|
5
|
+
|
|
6
|
+
// Only export creation functions, not types
|
|
7
|
+
export { createBasePlugin } from "./lib/BasePlugin"
|
|
8
|
+
export { createMonitoringPlugin } from "./lib/MonitoringPlugin"
|
|
9
|
+
|
|
10
|
+
// Export the PM2 plugin factory function
|
|
11
|
+
export { createPM2Plugin } from "./plugins/monitoring/PM2Plugin"
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daemonitor/plugins",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "daemonitor plugins",
|
|
5
5
|
"types": "types/index.d.ts",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"scripts": {
|
|
8
9
|
"build": "tsc",
|
|
9
10
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -12,24 +13,42 @@
|
|
|
12
13
|
"dist",
|
|
13
14
|
"types"
|
|
14
15
|
],
|
|
15
|
-
"
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.ts",
|
|
18
|
+
"./lib/*": "./lib/*.ts",
|
|
19
|
+
"./plugins/*": "./plugins/*.ts"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "types/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./types/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./lib/*": "./dist/lib/*.js",
|
|
31
|
+
"./plugins/*": "./dist/plugins/*.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
16
34
|
"author": "",
|
|
17
35
|
"license": "ISC",
|
|
18
36
|
"packageManager": "pnpm@8.2.0",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"nuxt": "^3.15.4",
|
|
39
|
+
"vue": "^3.5.13",
|
|
40
|
+
"vue-router": "^4.5.0"
|
|
41
|
+
},
|
|
19
42
|
"dependencies": {
|
|
20
43
|
"dotenv": "^16.0.3",
|
|
21
44
|
"ewelink-api": "^3.1.1",
|
|
22
45
|
"node-machine-id": "^1.1.12",
|
|
23
46
|
"ohmyfetch": "^0.4.21",
|
|
24
|
-
"pm2": "^5.1.0",
|
|
25
47
|
"string-to-color": "^2.2.2",
|
|
26
48
|
"ts-node": "^10.9.1",
|
|
27
49
|
"@daemonitor/common": "^1.0.0",
|
|
28
|
-
"tsconfig-paths": "^4.2.0"
|
|
29
|
-
|
|
30
|
-
"exports": {
|
|
31
|
-
".": "./dist/index.js",
|
|
32
|
-
"./types": "./dist/types/index.js"
|
|
50
|
+
"tsconfig-paths": "^4.2.0",
|
|
51
|
+
"pm2": "^6.0.5"
|
|
33
52
|
},
|
|
34
53
|
"repository": {
|
|
35
54
|
"type": "git",
|
|
@@ -38,6 +57,5 @@
|
|
|
38
57
|
"bugs": {
|
|
39
58
|
"url": "https://github.com/daemonitor/plugins/issues"
|
|
40
59
|
},
|
|
41
|
-
|
|
42
60
|
"homepage": "https://github.com/daemonitor/plugins#readme"
|
|
43
61
|
}
|