@oas-tools/oas-telemetry 0.7.0-alpha.3 → 0.7.0-alpha.4
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 +7 -4
- package/dist/cjs/telemetry/custom-implementations/exporters/PluginLogExporter.cjs +2 -9
- package/dist/cjs/telemetry/custom-implementations/exporters/PluginMetricExporter.cjs +1 -8
- package/dist/cjs/telemetry/custom-implementations/exporters/PluginSpanExporter.cjs +2 -10
- package/dist/cjs/tlm-plugin/pluginController.cjs +102 -84
- package/dist/cjs/tlm-plugin/pluginProcess.cjs +108 -0
- package/dist/cjs/tlm-plugin/pluginRoutes.cjs +3 -0
- package/dist/cjs/tlm-plugin/pluginService.cjs +58 -4
- package/dist/esm/telemetry/custom-implementations/exporters/PluginLogExporter.js +2 -10
- package/dist/esm/telemetry/custom-implementations/exporters/PluginMetricExporter.js +1 -9
- package/dist/esm/telemetry/custom-implementations/exporters/PluginSpanExporter.js +2 -11
- package/dist/esm/tlm-plugin/pluginController.js +101 -87
- package/dist/esm/tlm-plugin/pluginProcess.js +101 -0
- package/dist/esm/tlm-plugin/pluginRoutes.js +4 -1
- package/dist/esm/tlm-plugin/pluginService.js +58 -4
- package/dist/types/config/config.d.ts +610 -5
- package/dist/types/tlm-plugin/pluginController.d.ts +4 -1
- package/dist/types/tlm-plugin/pluginProcess.d.ts +1 -0
- package/dist/types/tlm-plugin/pluginService.d.ts +17 -2
- package/dist/types/types/index.d.ts +14 -5
- package/dist/ui/assets/index-BzIdRox6.js +1733 -0
- package/dist/ui/assets/index-CkoHzrrt.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-D9HsRlaQ.js +0 -437
- package/dist/ui/assets/index-DEyIcKBi.css +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# OAS TELEMETRY
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
**OAS Telemetry** is a library that automatically configures telemetry in your Express application based on OpenAPI, with no extra code required. Simply use the middleware to instantly access endpoints for viewing recent requests, system logs, and metrics—all stored in memory. This allows you to analyze your API’s behavior and debug issues easily, without manual setup or complex integration. OpenTelemetry is used under the hood to collect traces, metrics, and logs.
|
|
4
5
|
|
|
5
6
|
The middleware is highly configurable and supports both **ES Module (ESM)** and **CommonJS (CJS)** formats (ESM targeting **ES2020**). Its functionality can be extended via plugins; see [Telemetry Plugins](#telemetry-plugins) for details.
|
|
6
7
|
|
|
@@ -8,11 +9,13 @@ The middleware is highly configurable and supports both **ES Module (ESM)** and
|
|
|
8
9
|
>
|
|
9
10
|
> **OAS Telemetry** is a functional and working package, but it is currently at version 0 and remains under active development. Features, APIs, and behavior are subject to change at any time. Please review the following current status before use:
|
|
10
11
|
>
|
|
11
|
-
> - **Traces:** Semi-stable. Currently supports HTTP instrumentation.
|
|
12
|
+
> - **Traces:** Semi-stable. Currently supports HTTP instrumentation. We are studying switching to auto-instrumentation, but we need to test memory usage and performance first.
|
|
12
13
|
> - **Logs:** Semi-stable. Supports fast search by message content and Mongo-like search (similar to traces).
|
|
13
|
-
> - **Metrics:**
|
|
14
|
+
> - **Metrics:** Not stable. Currently uses OpenTelemetry host metrics. This area is subject to change to improve memory usage and data handling.
|
|
14
15
|
> - **Configuration:** Semi-stable. All configuration options will be available as parameters at initialization and via environment variables (see the `.env.example` file). The configuration system is under active development and will change significantly in future releases.
|
|
15
|
-
> - **UI:** Not stable. Migration to React is in progress. Most views are placeholders except for the AI agent, which is fully functional and can answer questions about
|
|
16
|
+
> - **UI:** Not stable. Migration to React is in progress. Most views are placeholders except for the AI agent, which is fully functional and can answer questions about traces, and logs (Not yet for metrics). Plugin management page is also functional. Next steps include logs page (almost done) and traces page (subject to change based on instrumentation approach). Metrics page will be the last to be implemented, as we want to support custom metrics in the future.
|
|
17
|
+
>
|
|
18
|
+
> Please if you want to use this package contact us via motero6@us.es
|
|
16
19
|
|
|
17
20
|
## Usage
|
|
18
21
|
|
|
@@ -24,15 +24,8 @@ class PluginLogExporter extends _wrappers.Enabler {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
const cleanLogs = logs.map(log => (0, _circular.removeCircularRefs)(log)).map(log => (0, _circular.applyNesting)(log));
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
cleanLogs.forEach(log => {
|
|
30
|
-
_logger.default.debug(`Sending log to plugin (Plugin #${i}) <${pluginResource.name}>`);
|
|
31
|
-
pluginResource.pluginImplementation.newLog(log);
|
|
32
|
-
});
|
|
33
|
-
} else {
|
|
34
|
-
_logger.default.debug(`Plugin <${pluginResource.name}> does not implement newLog method. Skipping log export.`);
|
|
35
|
-
}
|
|
27
|
+
cleanLogs.forEach(log => {
|
|
28
|
+
_pluginService.pluginService.broadcastLog(log);
|
|
36
29
|
});
|
|
37
30
|
setTimeout(() => resultCallback({
|
|
38
31
|
code: _core.ExportResultCode.SUCCESS
|
|
@@ -24,14 +24,7 @@ class PluginMetricExporter extends _wrappers.Enabler {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
const cleanMetrics = (0, _circular.applyNesting)((0, _circular.removeCircularRefs)(metrics));
|
|
27
|
-
_pluginService.pluginService.
|
|
28
|
-
if (typeof pluginResource.pluginImplementation.newMetric === 'function') {
|
|
29
|
-
_logger.default.debug(`Sending metric to plugin (Plugin #${i}) <${pluginResource.name}>`);
|
|
30
|
-
pluginResource.pluginImplementation.newMetric(cleanMetrics);
|
|
31
|
-
} else {
|
|
32
|
-
_logger.default.debug(`Plugin <${pluginResource.name}> does not implement newMetric method. Skipping metric export.`);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
27
|
+
_pluginService.pluginService.broadcastMetric(cleanMetrics);
|
|
35
28
|
setTimeout(() => resultCallback({
|
|
36
29
|
code: _core.ExportResultCode.SUCCESS
|
|
37
30
|
}), 0);
|
|
@@ -37,16 +37,8 @@ class PluginSpanExporter extends _wrappers.Enabler {
|
|
|
37
37
|
}
|
|
38
38
|
return true;
|
|
39
39
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
cleanSpans.forEach(span => {
|
|
43
|
-
_logger.default.debug(`Sending span to plugin (Plugin #${i}) <${pluginResource.name}>`);
|
|
44
|
-
//TODO: This should be called newSpan instead of newTrace
|
|
45
|
-
pluginResource.pluginImplementation.newTrace(span);
|
|
46
|
-
});
|
|
47
|
-
} else {
|
|
48
|
-
_logger.default.debug(`Plugin <${pluginResource.name}> does not implement newTrace method. Skipping span export.`);
|
|
49
|
-
}
|
|
40
|
+
cleanSpans.forEach(span => {
|
|
41
|
+
_pluginService.pluginService.broadcastTrace(span);
|
|
50
42
|
});
|
|
51
43
|
setTimeout(() => resultCallback({
|
|
52
44
|
code: _core.ExportResultCode.SUCCESS
|
|
@@ -3,127 +3,145 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.registerPlugin = exports.listPlugins = void 0;
|
|
6
|
+
exports.registerPlugin = exports.listPlugins = exports.deletePlugin = exports.deactivatePlugin = exports.activatePlugin = void 0;
|
|
7
7
|
var _axios = _interopRequireDefault(require("axios"));
|
|
8
|
-
var
|
|
9
|
-
var
|
|
8
|
+
var _child_process = require("child_process");
|
|
9
|
+
var _path = _interopRequireDefault(require("path"));
|
|
10
10
|
var _logger = _interopRequireDefault(require("../utils/logger.cjs"));
|
|
11
11
|
var _pluginService = require("./pluginService.cjs");
|
|
12
|
+
var _url = require("url");
|
|
12
13
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
-
// @ts-expect-error: import-from-string does not have proper type declarations
|
|
14
|
-
|
|
15
|
-
// @ts-expect-error: dynamic-installer does not have proper type declarations
|
|
16
|
-
|
|
17
14
|
const listPlugins = (req, res) => {
|
|
18
|
-
const plugins = _pluginService.pluginService.getPlugins()
|
|
19
|
-
return {
|
|
20
|
-
id: plugin.id,
|
|
21
|
-
name: plugin.name,
|
|
22
|
-
url: plugin.url,
|
|
23
|
-
active: plugin.active
|
|
24
|
-
};
|
|
25
|
-
});
|
|
15
|
+
const plugins = _pluginService.pluginService.getPlugins();
|
|
26
16
|
res.send({
|
|
27
17
|
pluginsCount: plugins.length,
|
|
28
|
-
plugins
|
|
18
|
+
plugins
|
|
29
19
|
});
|
|
30
20
|
};
|
|
31
21
|
exports.listPlugins = listPlugins;
|
|
32
22
|
const registerPlugin = async (req, res) => {
|
|
33
|
-
let pluginCode;
|
|
34
23
|
const pluginResource = req.body;
|
|
35
|
-
_logger.default.debug(`Plugin Registration Request:
|
|
36
|
-
// Validate
|
|
24
|
+
_logger.default.debug(`Plugin Registration Request: ${JSON.stringify(req.body, null, 2)}...`);
|
|
25
|
+
// Validate id
|
|
37
26
|
if (!pluginResource.id || typeof pluginResource.id !== "string") {
|
|
38
|
-
res.status(400).send(
|
|
27
|
+
res.status(400).send("Plugin id must be provided and must be a string");
|
|
39
28
|
return;
|
|
40
29
|
}
|
|
41
|
-
// Check
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
res.status(400).send(`A plugin with id "${pluginResource.id}" already exists.`);
|
|
30
|
+
// Check duplicate
|
|
31
|
+
if (_pluginService.pluginService.getPlugins().find(p => p.id === pluginResource.id)) {
|
|
32
|
+
res.status(400).send(`Plugin with id "${pluginResource.id}" already exists.`);
|
|
45
33
|
return;
|
|
46
34
|
}
|
|
35
|
+
// Validate inputs
|
|
47
36
|
if (!pluginResource.url && !pluginResource.code) {
|
|
48
|
-
res.status(400).send(
|
|
37
|
+
res.status(400).send("Plugin code or URL must be provided");
|
|
49
38
|
return;
|
|
50
39
|
}
|
|
51
40
|
if (!pluginResource.moduleFormat) {
|
|
52
|
-
res.status(400).send(
|
|
41
|
+
res.status(400).send("Plugin moduleFormat must be provided (cjs|esm)");
|
|
53
42
|
return;
|
|
54
43
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
let module;
|
|
44
|
+
// Fetch code
|
|
45
|
+
let pluginCode;
|
|
60
46
|
try {
|
|
61
47
|
if (pluginResource.code) {
|
|
62
48
|
pluginCode = pluginResource.code;
|
|
63
49
|
} else {
|
|
50
|
+
console.log(pluginResource.url);
|
|
64
51
|
const response = await _axios.default.get(pluginResource.url);
|
|
65
52
|
pluginCode = response.data;
|
|
66
53
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
if (pluginResource.install) {
|
|
72
|
-
_logger.default.info("Installing dependencies for plugin: " + pluginResource.name);
|
|
73
|
-
const dependenciesStatus = await (0, _dynamicInstaller.installDependencies)(pluginResource.install);
|
|
74
|
-
if (!dependenciesStatus.success) {
|
|
75
|
-
if (pluginResource.install.ignoreErrors === true) {
|
|
76
|
-
_logger.default.warn(`Warning: Error installing dependencies: ${JSON.stringify(dependenciesStatus.details)}`);
|
|
77
|
-
} else {
|
|
78
|
-
res.status(400).send(`Error installing dependencies: ${JSON.stringify(dependenciesStatus.details)}`);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
_logger.default.debug("Plugin format (provided): " + pluginResource?.moduleFormat);
|
|
84
|
-
if (pluginResource.moduleFormat.toLowerCase() === "esm") {
|
|
85
|
-
_logger.default.info("ESM detected");
|
|
86
|
-
module = await (0, _importFromString.importFromString)(pluginCode);
|
|
87
|
-
} else {
|
|
88
|
-
_logger.default.info("CJS detected (default)");
|
|
89
|
-
module = await (0, _importFromString.requireFromString)(pluginCode);
|
|
90
|
-
}
|
|
91
|
-
} catch (error) {
|
|
92
|
-
_logger.default.error(`Error loading plugin: ${error}`);
|
|
93
|
-
res.status(400).send(`Error loading plugin: ${error}`);
|
|
54
|
+
pluginResource.sourceCode = pluginCode;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
res.status(400).send(`Error fetching plugin code: ${err}`);
|
|
94
57
|
return;
|
|
95
58
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
res.status(400).send(`Plugin code should export a valid "plugin" object or static class`);
|
|
99
|
-
_logger.default.info("Error in plugin code: no valid plugin object exported");
|
|
59
|
+
if (!pluginCode) {
|
|
60
|
+
res.status(400).send("Plugin code could not be loaded");
|
|
100
61
|
return;
|
|
101
62
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
63
|
+
const isCjs = typeof __filename !== "undefined" && typeof __dirname !== "undefined";
|
|
64
|
+
const __filenameUniversal = isCjs ? __filename : (0, _url.fileURLToPath)(require('url').pathToFileURL(__filename).toString());
|
|
65
|
+
const __dirnameUniversal = isCjs ? __dirname : _path.default.dirname(__filenameUniversal);
|
|
66
|
+
const pluginProcessFile = isCjs ? "pluginProcess.cjs" : "pluginProcess.js";
|
|
67
|
+
const child = (0, _child_process.fork)(_path.default.resolve(__dirnameUniversal, pluginProcessFile), [], {
|
|
68
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"]
|
|
69
|
+
});
|
|
70
|
+
child.stdout?.on("data", data => {
|
|
71
|
+
_logger.default.info(`[Plugin ${pluginResource.id}] STDOUT: ${data.toString().trim()}`);
|
|
72
|
+
});
|
|
73
|
+
child.stderr?.on("data", data => {
|
|
74
|
+
_logger.default.error(`[Plugin ${pluginResource.id}] STDERR: ${data.toString().trim()}`);
|
|
75
|
+
});
|
|
76
|
+
child.on("message", msg => {
|
|
77
|
+
if (msg.event === "loaded") {
|
|
78
|
+
pluginResource.name = msg.name;
|
|
79
|
+
pluginResource.active = true;
|
|
80
|
+
pluginResource.process = child;
|
|
81
|
+
_pluginService.pluginService.pushPlugin(pluginResource);
|
|
82
|
+
res.status(201).send(`Plugin ${msg.name} registered`);
|
|
83
|
+
} else if (msg.event === "error") {
|
|
84
|
+
res.status(400).send(`Error loading plugin: ${msg.error}`);
|
|
107
85
|
}
|
|
86
|
+
});
|
|
87
|
+
child.on("exit", code => {
|
|
88
|
+
pluginResource.active = false;
|
|
89
|
+
pluginResource.process = undefined;
|
|
90
|
+
_logger.default.warn(`Plugin ${pluginResource.id} exited (code: ${code})`);
|
|
91
|
+
});
|
|
92
|
+
child.on("disconnect", () => {
|
|
93
|
+
pluginResource.active = false;
|
|
94
|
+
pluginResource.process = undefined;
|
|
95
|
+
_logger.default.warn(`Plugin ${pluginResource.id} disconnected`);
|
|
96
|
+
});
|
|
97
|
+
child.on("error", err => {
|
|
98
|
+
pluginResource.active = false;
|
|
99
|
+
pluginResource.process = undefined;
|
|
100
|
+
_logger.default.error(`Plugin ${pluginResource.id} error: ${err.message}`);
|
|
101
|
+
});
|
|
102
|
+
// Send data to child
|
|
103
|
+
child.send({
|
|
104
|
+
type: "load",
|
|
105
|
+
pluginResource
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
exports.registerPlugin = registerPlugin;
|
|
109
|
+
const activatePlugin = (req, res) => {
|
|
110
|
+
const {
|
|
111
|
+
id
|
|
112
|
+
} = req.params;
|
|
113
|
+
const plugin = _pluginService.pluginService.getPlugins().find(p => p.id === id);
|
|
114
|
+
if (!plugin) {
|
|
115
|
+
res.status(404).send(`Plugin with id "${id}" not found.`);
|
|
116
|
+
return;
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
_pluginService.pluginService.activatePlugin(id);
|
|
119
|
+
res.status(200).send(`Plugin "${id}" activated.`);
|
|
120
|
+
};
|
|
121
|
+
exports.activatePlugin = activatePlugin;
|
|
122
|
+
const deactivatePlugin = (req, res) => {
|
|
123
|
+
const {
|
|
124
|
+
id
|
|
125
|
+
} = req.params;
|
|
126
|
+
const plugin = _pluginService.pluginService.getPlugins().find(p => p.id === id);
|
|
127
|
+
if (!plugin) {
|
|
128
|
+
res.status(404).send(`Plugin with id "${id}" not found.`);
|
|
114
129
|
return;
|
|
115
130
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
res.status(
|
|
131
|
+
_pluginService.pluginService.deactivatePlugin(id); // This only sets active to false
|
|
132
|
+
res.status(200).send(`Plugin "${id}" deactivated.`);
|
|
133
|
+
};
|
|
134
|
+
exports.deactivatePlugin = deactivatePlugin;
|
|
135
|
+
const deletePlugin = (req, res) => {
|
|
136
|
+
const {
|
|
137
|
+
id
|
|
138
|
+
} = req.params;
|
|
139
|
+
const plugin = _pluginService.pluginService.getPlugins().find(p => p.id === id);
|
|
140
|
+
if (!plugin) {
|
|
141
|
+
res.status(404).send(`Plugin with id "${id}" not found.`);
|
|
142
|
+
return;
|
|
127
143
|
}
|
|
144
|
+
_pluginService.pluginService.deletePlugin(id); // kills child inside service
|
|
145
|
+
res.status(200).send(`Plugin "${id}" deleted.`);
|
|
128
146
|
};
|
|
129
|
-
exports.
|
|
147
|
+
exports.deletePlugin = deletePlugin;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _importFromString = require("import-from-string");
|
|
4
|
+
var _dynamicInstaller = require("dynamic-installer");
|
|
5
|
+
// pluginProcess.js
|
|
6
|
+
// Runs inside a child process, isolated from the main app
|
|
7
|
+
// @ts-expect-error no types
|
|
8
|
+
|
|
9
|
+
let plugin;
|
|
10
|
+
const log = (...args) => {
|
|
11
|
+
console.log(`[PluginProcess:${process.pid}]`, ...args);
|
|
12
|
+
};
|
|
13
|
+
process.on("message", async msg => {
|
|
14
|
+
if (msg.type === "load") {
|
|
15
|
+
try {
|
|
16
|
+
const pluginResource = normalizePluginResource(msg.pluginResource);
|
|
17
|
+
if (pluginResource.install && Array.isArray(pluginResource.install.dependencies) && pluginResource.install.dependencies.length > 0) {
|
|
18
|
+
log("Installing dependencies for plugin: " + pluginResource.name);
|
|
19
|
+
const dependenciesStatus = await (0, _dynamicInstaller.installDependencies)(pluginResource.install);
|
|
20
|
+
if (!dependenciesStatus.success) {
|
|
21
|
+
if (pluginResource.install.ignoreErrors === true) {
|
|
22
|
+
log(`Warning: Error installing dependencies: ${JSON.stringify(dependenciesStatus.details)}`);
|
|
23
|
+
} else {
|
|
24
|
+
process.send?.({
|
|
25
|
+
event: "error",
|
|
26
|
+
error: `Error installing dependencies: ${JSON.stringify(dependenciesStatus.details)}`
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let module;
|
|
33
|
+
if (pluginResource?.moduleFormat?.toLowerCase() === "esm") {
|
|
34
|
+
module = await (0, _importFromString.importFromString)(pluginResource.sourceCode);
|
|
35
|
+
} else {
|
|
36
|
+
module = await (0, _importFromString.requireFromString)(pluginResource.sourceCode);
|
|
37
|
+
}
|
|
38
|
+
plugin = module.default?.plugin ?? module.plugin;
|
|
39
|
+
if (!plugin) throw new Error("Plugin must export a valid 'plugin' object");
|
|
40
|
+
for (const fn of ["load", "isConfigured"]) {
|
|
41
|
+
if (typeof plugin[fn] !== "function") {
|
|
42
|
+
throw new Error(`Plugin is missing required function "${fn}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await plugin.load(pluginResource.config);
|
|
46
|
+
if (!plugin.isConfigured()) {
|
|
47
|
+
throw new Error("Plugin could not be configured");
|
|
48
|
+
}
|
|
49
|
+
process.send?.({
|
|
50
|
+
event: "loaded",
|
|
51
|
+
name: pluginResource.name || pluginResource.id || "unknown"
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
process.send?.({
|
|
55
|
+
event: "error",
|
|
56
|
+
error: err.message
|
|
57
|
+
});
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Forward log/metric/trace calls
|
|
62
|
+
if (msg.type === "newLog" && plugin?.newLog) {
|
|
63
|
+
plugin.newLog(msg.payload);
|
|
64
|
+
}
|
|
65
|
+
if (msg.type === "newMetric" && plugin?.newMetric) {
|
|
66
|
+
plugin.newMetric(msg.payload);
|
|
67
|
+
}
|
|
68
|
+
if (msg.type === "newTrace" && plugin?.newTrace) {
|
|
69
|
+
plugin.newTrace(msg.payload);
|
|
70
|
+
}
|
|
71
|
+
if (msg.type === "unload") {
|
|
72
|
+
if (plugin && typeof plugin.unload === "function") {
|
|
73
|
+
await plugin.unload();
|
|
74
|
+
}
|
|
75
|
+
process.send?.({
|
|
76
|
+
event: "unloaded"
|
|
77
|
+
});
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
function normalizePluginResource(raw) {
|
|
82
|
+
let resource = raw;
|
|
83
|
+
// case: received as stringified JSON
|
|
84
|
+
if (typeof raw === "string") {
|
|
85
|
+
try {
|
|
86
|
+
resource = JSON.parse(raw);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error("Invalid pluginResource JSON: " + err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// normalize install
|
|
92
|
+
if (typeof resource.install === "string") {
|
|
93
|
+
try {
|
|
94
|
+
resource.install = JSON.parse(resource.install);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw new Error("Invalid install JSON: " + err.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// normalize config
|
|
100
|
+
if (typeof resource.config === "string") {
|
|
101
|
+
try {
|
|
102
|
+
resource.config = JSON.parse(resource.config);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw new Error("Invalid config JSON: " + err.message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return resource;
|
|
108
|
+
}
|
|
@@ -10,6 +10,9 @@ const getPluginRoutes = () => {
|
|
|
10
10
|
const router = (0, _express.Router)();
|
|
11
11
|
router.get('/', _pluginController.listPlugins);
|
|
12
12
|
router.post('/', _pluginController.registerPlugin);
|
|
13
|
+
router.post('/:id/activate', _pluginController.activatePlugin);
|
|
14
|
+
router.post('/:id/deactivate', _pluginController.deactivatePlugin);
|
|
15
|
+
router.delete('/:id', _pluginController.deletePlugin);
|
|
13
16
|
return router;
|
|
14
17
|
};
|
|
15
18
|
exports.getPluginRoutes = getPluginRoutes;
|
|
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.pluginService = void 0;
|
|
7
|
+
var _logger = _interopRequireDefault(require("../utils/logger.cjs"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
9
|
class PluginService {
|
|
8
10
|
constructor() {
|
|
9
11
|
this.plugins = [];
|
|
@@ -14,12 +16,64 @@ class PluginService {
|
|
|
14
16
|
pushPlugin(plugin) {
|
|
15
17
|
this.plugins.push(plugin);
|
|
16
18
|
}
|
|
17
|
-
activatePlugin(
|
|
18
|
-
this.plugins.
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
activatePlugin(pluginId) {
|
|
20
|
+
const plugin = this.plugins.find(p => p.id === pluginId);
|
|
21
|
+
if (plugin) {
|
|
22
|
+
plugin.active = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
deactivatePlugin(pluginId) {
|
|
26
|
+
const plugin = this.plugins.find(p => p.id === pluginId);
|
|
27
|
+
if (plugin) {
|
|
28
|
+
plugin.active = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
deletePlugin(pluginId) {
|
|
32
|
+
const plugin = this.plugins.find(p => p.id === pluginId);
|
|
33
|
+
if (plugin?.process && !plugin.process.killed) {
|
|
34
|
+
plugin.process.kill(1);
|
|
35
|
+
}
|
|
36
|
+
this.plugins = this.plugins.filter(p => p.id !== pluginId);
|
|
37
|
+
}
|
|
38
|
+
broadcastToPlugins(type, payload) {
|
|
39
|
+
this.plugins.forEach((plugin, i) => {
|
|
40
|
+
if (!plugin.active) return;
|
|
41
|
+
if (plugin.process) {
|
|
42
|
+
if (plugin.process.connected) {
|
|
43
|
+
try {
|
|
44
|
+
plugin.process.send({
|
|
45
|
+
type,
|
|
46
|
+
payload
|
|
47
|
+
});
|
|
48
|
+
_logger.default.debug(`Sent ${type} to child-process plugin <${plugin.name}> (#${i})`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
_logger.default.error(`Failed to send ${type} to plugin <${plugin.name}> (#${i}):`, err);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
_logger.default.warn(`Plugin <${plugin.name}> (#${i}) is not connected. Skipping ${type}.`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
_logger.default.debug(`Plugin <${plugin.name}> does not implement ${type}. Skipping.`);
|
|
21
57
|
}
|
|
22
58
|
});
|
|
23
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Broadcast a new metric to all active plugins
|
|
62
|
+
*/
|
|
63
|
+
broadcastMetric(metric) {
|
|
64
|
+
this.broadcastToPlugins("newMetric", metric);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Broadcast a new log to all active plugins
|
|
68
|
+
*/
|
|
69
|
+
broadcastLog(log) {
|
|
70
|
+
this.broadcastToPlugins("newLog", log);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Broadcast a new trace to all active plugins
|
|
74
|
+
*/
|
|
75
|
+
broadcastTrace(trace) {
|
|
76
|
+
this.broadcastToPlugins("newTrace", trace);
|
|
77
|
+
}
|
|
24
78
|
}
|
|
25
79
|
const pluginService = exports.pluginService = new PluginService();
|
|
@@ -17,16 +17,8 @@ export class PluginLogExporter extends Enabler {
|
|
|
17
17
|
const cleanLogs = logs
|
|
18
18
|
.map(log => removeCircularRefs(log))
|
|
19
19
|
.map(log => applyNesting(log));
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
cleanLogs.forEach((log) => {
|
|
23
|
-
logger.debug(`Sending log to plugin (Plugin #${i}) <${pluginResource.name}>`);
|
|
24
|
-
pluginResource.pluginImplementation.newLog(log);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
logger.debug(`Plugin <${pluginResource.name}> does not implement newLog method. Skipping log export.`);
|
|
29
|
-
}
|
|
20
|
+
cleanLogs.forEach(log => {
|
|
21
|
+
pluginService.broadcastLog(log);
|
|
30
22
|
});
|
|
31
23
|
setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0);
|
|
32
24
|
}
|
|
@@ -15,15 +15,7 @@ export class PluginMetricExporter extends Enabler {
|
|
|
15
15
|
return resultCallback({ code: ExportResultCode.SUCCESS });
|
|
16
16
|
}
|
|
17
17
|
const cleanMetrics = applyNesting(removeCircularRefs(metrics));
|
|
18
|
-
pluginService.
|
|
19
|
-
if (typeof pluginResource.pluginImplementation.newMetric === 'function') {
|
|
20
|
-
logger.debug(`Sending metric to plugin (Plugin #${i}) <${pluginResource.name}>`);
|
|
21
|
-
pluginResource.pluginImplementation.newMetric(cleanMetrics);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
logger.debug(`Plugin <${pluginResource.name}> does not implement newMetric method. Skipping metric export.`);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
18
|
+
pluginService.broadcastMetric(cleanMetrics);
|
|
27
19
|
setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0);
|
|
28
20
|
}
|
|
29
21
|
catch (error) {
|
|
@@ -30,17 +30,8 @@ export class PluginSpanExporter extends Enabler {
|
|
|
30
30
|
}
|
|
31
31
|
return true;
|
|
32
32
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
cleanSpans.forEach((span) => {
|
|
36
|
-
logger.debug(`Sending span to plugin (Plugin #${i}) <${pluginResource.name}>`);
|
|
37
|
-
//TODO: This should be called newSpan instead of newTrace
|
|
38
|
-
pluginResource.pluginImplementation.newTrace(span);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
logger.debug(`Plugin <${pluginResource.name}> does not implement newTrace method. Skipping span export.`);
|
|
43
|
-
}
|
|
33
|
+
cleanSpans.forEach(span => {
|
|
34
|
+
pluginService.broadcastTrace(span);
|
|
44
35
|
});
|
|
45
36
|
setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0);
|
|
46
37
|
}
|