@oas-tools/oas-telemetry 0.3.0 → 0.5.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.
Files changed (39) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +151 -133
  3. package/dist/client.cjs +14 -0
  4. package/dist/config.cjs +27 -0
  5. package/dist/controllers/pluginController.cjs +118 -0
  6. package/dist/controllers/telemetryController.cjs +92 -0
  7. package/dist/controllers/uiController.cjs +78 -0
  8. package/dist/exporters/InMemoryDbExporter.cjs +45 -42
  9. package/dist/exporters/consoleExporter.cjs +52 -0
  10. package/dist/exporters/dynamicExporter.cjs +64 -0
  11. package/dist/index.cjs +61 -248
  12. package/dist/middleware/auth.cjs +17 -0
  13. package/dist/middleware/authMiddleware.cjs +19 -0
  14. package/dist/openTelemetry.cjs +20 -0
  15. package/dist/routes/authRoutes.cjs +79 -0
  16. package/dist/routes/telemetryRoutes.cjs +31 -0
  17. package/{src/ui.js → dist/services/uiService.cjs} +1140 -813
  18. package/dist/telemetry.cjs +0 -0
  19. package/dist/types/exporters/InMemoryDbExporter.d.ts +16 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/telemetry.d.ts +2 -0
  22. package/dist/types/ui.d.ts +4 -0
  23. package/dist/ui.cjs +0 -0
  24. package/package.json +75 -71
  25. package/src/config.js +19 -0
  26. package/src/controllers/pluginController.js +115 -0
  27. package/src/controllers/telemetryController.js +68 -0
  28. package/src/controllers/uiController.js +69 -0
  29. package/src/dev/ui/login.html +32 -0
  30. package/src/exporters/InMemoryDbExporter.js +180 -175
  31. package/src/exporters/consoleExporter.js +47 -0
  32. package/src/exporters/dynamicExporter.js +62 -0
  33. package/src/index.js +85 -307
  34. package/src/middleware/authMiddleware.js +14 -0
  35. package/src/openTelemetry.js +22 -0
  36. package/src/routes/authRoutes.js +53 -0
  37. package/src/routes/telemetryRoutes.js +38 -0
  38. package/src/services/uiService.js +1520 -0
  39. package/src/telemetry.js +0 -25
package/src/index.js CHANGED
@@ -1,307 +1,85 @@
1
- // telemetryMiddleware.js
2
- import { inMemoryExporter } from './telemetry.js';
3
- import { Router, json } from 'express';
4
- import v8 from 'v8';
5
- import { readFileSync, existsSync } from 'fs';
6
- import path from 'path';
7
- import yaml from 'js-yaml';
8
- import ui from './ui.js'
9
- import axios from 'axios';
10
- import { requireFromString, importFromString } from "import-from-string";
11
- import { installDependencies } from "dynamic-installer"
12
-
13
- let dbglog = () => { };
14
-
15
- if (process.env.OTDEBUG == "true")
16
- dbglog = console.log;
17
-
18
- let plugins = [];
19
-
20
- let telemetryStatus = {
21
- active: true
22
- };
23
-
24
- let baseURL = '/telemetry';
25
-
26
- let telemetryConfig = {
27
- exporter: inMemoryExporter,
28
- specFileName: ""
29
- };
30
-
31
- export default function oasTelemetry(tlConfig) {
32
- if (tlConfig) {
33
- dbglog('Telemetry config provided');
34
- telemetryConfig = tlConfig;
35
- if (telemetryConfig.exporter == undefined)
36
- telemetryConfig.exporter = inMemoryExporter;
37
- }
38
-
39
- if (telemetryConfig.spec)
40
- dbglog(`Spec content provided`);
41
- else {
42
- if (telemetryConfig.specFileName != "")
43
- dbglog(`Spec file used for telemetry: ${telemetryConfig.specFileName}`);
44
- else {
45
- console.error("No spec available !");
46
- }
47
- }
48
-
49
- const router = Router();
50
-
51
- if (telemetryConfig.baseURL)
52
- baseURL = telemetryConfig.baseURL;
53
-
54
- router.use(json());
55
-
56
- router.get(baseURL, mainPage);
57
- router.get(baseURL + "/detail/*", detailPage);
58
- router.get(baseURL + "/spec", specLoader);
59
- router.get(baseURL + "/api", apiPage);
60
- router.get(baseURL + "/start", startTelemetry);
61
- router.get(baseURL + "/stop", stopTelemetry);
62
- router.get(baseURL + "/status", statusTelemetry);
63
- router.get(baseURL + "/reset", resetTelemetry);
64
- router.get(baseURL + "/list", listTelemetry);
65
- router.post(baseURL + "/find", findTelemetry);
66
- router.get(baseURL + "/heapStats", heapStats);
67
- router.get(baseURL + "/plugins", listPlugins);
68
- router.post(baseURL + "/plugins", registerPlugin);
69
- return router;
70
- }
71
-
72
- const apiPage = (req, res) => {
73
- let text = `
74
- <h1>Telemetry API routes:</h1>
75
- <ul>
76
- <li><a href="/telemetry/start">/telemetry/start</a></li>
77
- <li><a href="/telemetry/stop">/telemetry/stop</a></li>
78
- <li><a href="/telemetry/status">/telemetry/status</a></li>
79
- <li><a href="/telemetry/reset">/telemetry/reset</a></li>
80
- <li><a href="/telemetry/list">/telemetry/list</a></li>
81
- <li><a href="/telemetry/heapStats">/telemetry/heapStats</a></li>
82
- <li>/telemetry/find [POST]</li>
83
- </ul>
84
- `;
85
- res.send(text);
86
- }
87
-
88
- const mainPage = (req, res) => {
89
- res.set('Content-Type', 'text/html');
90
- res.send(ui().main);
91
- }
92
- const detailPage = (req, res) => {
93
- res.set('Content-Type', 'text/html');
94
- res.send(ui().detail);
95
- }
96
-
97
- const specLoader = (req, res) => {
98
- if (telemetryConfig.specFileName) {
99
- try {
100
- const data = readFileSync(telemetryConfig.specFileName,
101
- { encoding: 'utf8', flag: 'r' });
102
-
103
- const extension = path.extname(telemetryConfig.specFileName);
104
-
105
- let json = data;
106
-
107
- if (extension == yaml)
108
- json = JSON.stringify(yaml.SafeLoad(data), null, 2);
109
-
110
- res.setHeader('Content-Type', 'application/json');
111
- res.send(json);
112
-
113
- } catch (e) {
114
- console.error(`ERROR loading spec file ${telemetryConfig.specFileName}: ${e}`)
115
- }
116
- } else {
117
- if (telemetryConfig.spec) {
118
- let spec = false;
119
-
120
- try {
121
- spec = JSON.parse(telemetryConfig.spec);
122
- } catch (ej) {
123
- try {
124
- spec = JSON.stringify(yaml.load(telemetryConfig.spec), null, 2);
125
- } catch (ey) {
126
- console.error(`Error parsing spec: ${ej} - ${ey}`);
127
- }
128
- }
129
-
130
- if (!spec) {
131
- res.status(404);
132
- } else {
133
- res.setHeader('Content-Type', 'application/json');
134
- res.send(spec);
135
- }
136
-
137
- }
138
- }
139
- }
140
-
141
- const startTelemetry = (req, res) => {
142
- telemetryConfig.exporter.start();
143
- res.send('Telemetry started');
144
- }
145
- const stopTelemetry = (req, res) => {
146
- telemetryConfig.exporter.stop();
147
-
148
- res.send('Telemetry stopped');
149
- }
150
- const statusTelemetry = (req, res) => {
151
- const status = !telemetryConfig.exporter._stopped || false;
152
- res.send({ active: status });
153
- }
154
-
155
- const resetTelemetry = (req, res) => {
156
- telemetryConfig.exporter.reset();
157
- res.send('Telemetry reset');
158
- }
159
- const listTelemetry = (req, res) => {
160
- const spansDB = telemetryConfig.exporter.getFinishedSpans();
161
- spansDB.find({}, (err, docs) => {
162
- if (err) {
163
- console.error(err);
164
- return;
165
- }
166
- const spans = docs;
167
- res.send({ spansCount: spans.length, spans: spans });
168
- });
169
- }
170
-
171
- const heapStats = (req, res) => {
172
- var heapStats = v8.getHeapStatistics();
173
-
174
- // Round stats to MB
175
- var roundedHeapStats = Object.getOwnPropertyNames(heapStats).reduce(function (map, stat) {
176
- map[stat] = Math.round((heapStats[stat] / 1024 / 1024) * 1000) / 1000;
177
- return map;
178
- }, {});
179
- roundedHeapStats['units'] = 'MB';
180
-
181
- res.send(roundedHeapStats);
182
- }
183
-
184
- const findTelemetry = (req, res) => {
185
- const spansDB = telemetryConfig.exporter.getFinishedSpans();
186
- const body = req.body;
187
- const search = body?.search ? body.search : {};
188
- if (body?.flags?.containsRegex) {
189
- try {
190
- body.config?.regexIds?.forEach(regexId => {
191
- search[regexId] = new RegExp(search[regexId]);
192
- });
193
- } catch (e) {
194
- console.error(e);
195
- res.status(404).send({ spansCount: 0, spans: [], error: e });
196
- return;
197
- }
198
- spansDB.find(search, (err, docs) => {
199
- if (err) {
200
- console.error(err);
201
- res.status(404).send({ spansCount: 0, spans: [], error: err });
202
- return;
203
- }
204
-
205
- const spans = docs;
206
- res.send({ spansCount: spans.length, spans: spans });
207
- });
208
- }
209
- }
210
-
211
- const listPlugins = (req, res) => {
212
- res.send(plugins.map((p) => {
213
- return {
214
- id: p.id,
215
- url: p.url,
216
- active: p.active
217
- };
218
- }));
219
- }
220
-
221
- const registerPlugin = async (req, res) => {
222
- let pluginResource = req.body;
223
- dbglog(`Plugin Registration Request: = ${JSON.stringify(req.body, null, 2)}...`);
224
- dbglog(`Getting plugin at ${pluginResource.url}...`);
225
- let pluginCode;
226
- if (!pluginResource.url && !pluginResource.code) {
227
- res.status(400).send(`Plugin code or URL must be provided`);
228
- return;
229
- }
230
-
231
- let module;
232
- try {
233
- if (pluginResource.code) {
234
- pluginCode = pluginResource.code
235
- } else {
236
- const response = await axios.get(pluginResource.url);
237
- pluginCode = response.data;
238
- }
239
- if (!pluginCode) {
240
- res.status(400).send(`Plugin code could not be loaded`);
241
- return;
242
- }
243
- //install dependencies if any
244
- if (pluginResource.install) {
245
- const dependenciesStatus = await installDependencies(pluginResource.install);
246
- if (!dependenciesStatus.success) {
247
- if (pluginResource.install.ignoreErrors === true) {
248
- console.warn(`Warning: Error installing dependencies: ${JSON.stringify(dependenciesStatus.details)}`);
249
- } else {
250
- res.status(400).send(`Error installing dependencies: ${JSON.stringify(dependenciesStatus.details)}`);
251
- return;
252
- }
253
- }
254
- }
255
-
256
- dbglog("Plugin size: " + pluginCode?.length);
257
- if (pluginResource?.moduleFormat && pluginResource.moduleFormat.toUpperCase() == "ESM") {
258
- console.log("ESM detected")
259
- module = await importFromString(pluginCode)
260
- } else {
261
- console.log("CJS detected (default)")
262
- module = await requireFromString(pluginCode)
263
- console.log(module)
264
- }
265
- } catch (error) {
266
- console.error(`Error loading plugin: ${error}`);
267
- res.status(400).send(`Error loading plugin: ${error}`);
268
- return;
269
- }
270
-
271
- if (module.plugin == undefined) {
272
- res.status(400).send(`Plugin code should export a "plugin" object`);
273
- console.log("Error in plugin code: no plugin object exported")
274
- return;
275
- }
276
- for (let requiredFunction of ["load", "getName", "isConfigured"]) {
277
- if (module.plugin[requiredFunction] == undefined) {
278
- res.status(400).send(`The plugin code exports a "plugin" object, however it should have a "${requiredFunction}" method`);
279
- console.log("Error in plugin code: some required functions are missing")
280
- return;
281
- }
282
- }
283
-
284
- let plugin = module.plugin
285
- try {
286
- await plugin.load(pluginResource.config);
287
- } catch (error) {
288
- console.error(`Error loading plugin configuration: ${error}`);
289
- res.status(400).send(`Error loading plugin configuration: ${error}`);
290
- return;
291
- }
292
- if (plugin.isConfigured()) {
293
- dbglog(`Loaded plugin <${plugin.getName()}>`);
294
- pluginResource.plugin = plugin;
295
- pluginResource.name = plugin.getName();
296
- pluginResource.active = true;
297
- plugins.push(pluginResource);
298
- inMemoryExporter.activatePlugin(pluginResource.plugin);
299
- res.status(201).send(`Plugin registered`);
300
- } else {
301
- console.error(`Plugin <${plugin.getName()}> can not be configured`);
302
- res.status(400).send(`Plugin configuration problem`);
303
- }
304
-
305
-
306
- }
307
-
1
+ import './openTelemetry.js';
2
+ import { globalOasTlmConfig } from './config.js';
3
+ import cookieParser from 'cookie-parser';
4
+ import { Router, json } from 'express';
5
+ import { authMiddleware } from './middleware/authMiddleware.js';
6
+ import authRoutes from './routes/authRoutes.js';
7
+ import { telemetryRoutes } from './routes/telemetryRoutes.js';
8
+ import { InMemoryExporter } from './exporters/InMemoryDbExporter.js';
9
+
10
+
11
+ let dbglog = () => { };
12
+
13
+ if (process.env.OTDEBUG == "true")
14
+ dbglog = console.log;
15
+
16
+ /**
17
+ * Returns the Oas Telemetry middleware. The parameters are the same as `globalOasTlmConfig`.
18
+ * All parameters are optional. However, either `spec` or `specFileName` must be provided to enable endpoint filtering.
19
+ *
20
+ * @param {Object} OasTlmConfig Configuration object.
21
+ * @param {string} [OasTlmConfig.baseURL="/telemetry"] The base URL for the telemetry routes.
22
+ * @param {Object} [OasTlmConfig.spec] The OpenAPI spec object.
23
+ * @param {string} [OasTlmConfig.specFileName] Alternative to `spec`: the path to the OpenAPI spec file.
24
+ * @param {boolean} [OasTlmConfig.autoActivate=true] Whether to start telemetry automatically on load.
25
+ * @param {number} [OasTlmConfig.apiKeyMaxAge=1800000] The maximum age of the API key in milliseconds.
26
+ * @param {string} [OasTlmConfig.defaultApiKey] The default API key to use.
27
+ * @param {OasTlmExporter} [OasTlmConfig.exporter=InMemoryExporter] The exporter to use. Must implement the `OasTlmExporter` interface.
28
+ * @returns {Router} The middleware router for Oas Telemetry.
29
+ */
30
+ export default function oasTelemetry(OasTlmConfig) {
31
+ const router = Router();
32
+ if (process.env.OASTLM_MODULE_DISABLED === 'true') {
33
+ return router;
34
+ };
35
+ if (OasTlmConfig) {
36
+ console.log("User provided config");
37
+ // Global = user-provided || default, for each key
38
+ for (const key in globalOasTlmConfig) {
39
+ globalOasTlmConfig[key] = OasTlmConfig[key] ?? globalOasTlmConfig[key];
40
+ }
41
+ }
42
+ console.log("baseURL: ", globalOasTlmConfig.baseURL);
43
+ globalOasTlmConfig.dynamicExporter.changeExporter( OasTlmConfig.exporter ?? new InMemoryExporter() );
44
+
45
+ if (globalOasTlmConfig.spec)
46
+ dbglog(`Spec content provided`);
47
+ else {
48
+ if (globalOasTlmConfig.specFileName != "")
49
+ dbglog(`Spec file used for telemetry: ${globalOasTlmConfig.specFileName}`);
50
+ else {
51
+ console.error("No spec available !");
52
+ }
53
+ }
54
+
55
+ router.use(cookieParser());
56
+ const baseURL = globalOasTlmConfig.baseURL;
57
+ router.use(json());
58
+ router.use(baseURL, authRoutes);
59
+ router.use(baseURL, authMiddleware); // Add the auth middleware
60
+ router.use(baseURL, telemetryRoutes);
61
+
62
+ if (globalOasTlmConfig.autoActivate) {
63
+ globalOasTlmConfig.dynamicExporter.exporter?.start();
64
+ }
65
+
66
+ return router;
67
+ }
68
+
69
+
70
+
71
+ /**
72
+ * @typedef OasTlmExporter
73
+ * Represents an exporter that processes and manages telemetry data.
74
+ * Any custom exporter must implement these methods.
75
+ *
76
+ * @method {void} start() Starts the exporter, allowing it to process data.
77
+ * @method {void} stop() Stops the exporter and halts data processing.
78
+ * @method {void} reset() Resets the internal state of the exporter (e.g., clears buffers or data stores).
79
+ * @method {boolean} isRunning() Returns whether the exporter is actively processing data.
80
+ * @method {Array} getFinishedSpans() Retrieves the collected spans from the exporter.
81
+ * @method {any} export(ReadableSpan, SpanExporterResultCallback) Exports spans.
82
+ * @method {Promise<void>} shutdown() Gracefully shuts down the exporter, flushing data if necessary.
83
+ * @method {Promise<void>} forceFlush() Exports any pending data that has not yet been processed.
84
+ * @property {Array} plugins An array of plugins that can be activated by the exporter.
85
+ */
@@ -0,0 +1,14 @@
1
+ import { globalOasTlmConfig } from "../config.js";
2
+ import jwt from 'jsonwebtoken';
3
+
4
+ export function authMiddleware(req, res, next) {
5
+ const apiKey = req.cookies.apiKey;
6
+ if (apiKey) {
7
+ const decoded = jwt.verify(apiKey, globalOasTlmConfig.jwtSecret);
8
+ if (decoded.password === globalOasTlmConfig.password) {
9
+ return next();
10
+ }
11
+ }
12
+ res.status(401).redirect(globalOasTlmConfig.baseURL + '/login');
13
+
14
+ }
@@ -0,0 +1,22 @@
1
+
2
+ import { NodeSDK } from '@opentelemetry/sdk-node';
3
+ // import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
4
+ import { Resource } from '@opentelemetry/resources';
5
+ import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
6
+ import { globalOasTlmConfig } from './config.js';
7
+
8
+
9
+ // DynamicExporter allows changing to any exporter at runtime;
10
+ const traceExporter = globalOasTlmConfig.dynamicExporter;
11
+
12
+ const sdk = new NodeSDK({
13
+ resource: new Resource({
14
+ service: 'oas-telemetry-service'
15
+ }),
16
+ traceExporter,
17
+ instrumentations: [new HttpInstrumentation()]
18
+ });
19
+
20
+ if (process.env.OASTLM_MODULE_DISABLED !== 'true') {
21
+ sdk.start()
22
+ }
@@ -0,0 +1,53 @@
1
+ import { globalOasTlmConfig } from '../config.js';
2
+ import { Router } from 'express';
3
+ import { serialize } from 'cookie';
4
+ import ui from '../services/uiService.js';
5
+ import jwt from 'jsonwebtoken';
6
+
7
+ const router = Router();
8
+
9
+ router.post('/login', (req, res) => {
10
+ try {
11
+ const { password } = req.body;
12
+ if (password === globalOasTlmConfig.password) {
13
+ let options = {
14
+ maxAge: globalOasTlmConfig.apiKeyMaxAge,
15
+ httpOnly: true, // The cookie only accessible by the web server
16
+ secure: true, // Only sends cookie over https
17
+ signed: false // Indicates if the cookie should be signed
18
+ }
19
+ const apiKey = jwt.sign({ password: globalOasTlmConfig.password }, globalOasTlmConfig.jwtSecret);
20
+ res.cookie('apiKey', apiKey, options);
21
+ return res.status(200).json({ valid: true, message: 'API Key is valid' });
22
+ }
23
+ res.status(400).json({ valid: false, message: 'Invalid API Key' });
24
+
25
+ } catch (error) {
26
+ console.log("Error: ", error);
27
+ res.status(500).json({ valid: false, message: 'Internal server error' });
28
+ }
29
+ });
30
+
31
+ router.get('/logout', (req, res) => {
32
+ res.clearCookie('apiKey');
33
+ res.redirect(globalOasTlmConfig.baseURL + '/login');
34
+ });
35
+
36
+ //check is used in the UI (http polling) to validate the Cookies to redirect to the login page if needed
37
+ router.get('/check', (req, res) => {
38
+ if (!req.cookies.apiKey) {
39
+ return res.status(200).json({ valid: false, message: 'API Key is invalid' });
40
+ }
41
+ const decoded = jwt.verify(req.cookies.apiKey, globalOasTlmConfig.jwtSecret);
42
+ if (decoded.password === globalOasTlmConfig.password) {
43
+ return res.status(200).json({ valid: true, message: 'API Key is valid' });
44
+ }
45
+ res.status(200).json({ valid: false, message: 'Invalid API Key' });
46
+ });
47
+
48
+ router.get('/login', (req, res) => {
49
+ const baseURL = globalOasTlmConfig.baseURL;
50
+ res.send(ui(baseURL).login);
51
+ });
52
+
53
+ export default router;
@@ -0,0 +1,38 @@
1
+ import { Router } from 'express';
2
+ import { mainPage, detailPage , specLoader, apiPage} from '../controllers/uiController.js';
3
+ import {
4
+ startTelemetry,
5
+ stopTelemetry,
6
+ statusTelemetry,
7
+ resetTelemetry,
8
+ listTelemetry,
9
+ heapStats,
10
+ findTelemetry
11
+ } from '../controllers/telemetryController.js';
12
+ import { listPlugins, registerPlugin } from '../controllers/pluginController.js';
13
+
14
+ export const telemetryRoutes = Router();
15
+
16
+
17
+
18
+
19
+ // Main Pages
20
+ telemetryRoutes.get('/', mainPage);
21
+ telemetryRoutes.get('/detail/*', detailPage);
22
+ telemetryRoutes.get('/spec', specLoader);
23
+ telemetryRoutes.get('/api', apiPage);
24
+
25
+ // Telemetry Control
26
+ telemetryRoutes.get('/start', startTelemetry);
27
+ telemetryRoutes.get('/stop', stopTelemetry);
28
+ telemetryRoutes.get('/status', statusTelemetry);
29
+ telemetryRoutes.get('/reset', resetTelemetry);
30
+ telemetryRoutes.get('/list', listTelemetry);
31
+ telemetryRoutes.post('/find', findTelemetry);
32
+ telemetryRoutes.get('/heapStats', heapStats);
33
+
34
+ // Plugins
35
+ telemetryRoutes.get('/plugins', listPlugins);
36
+ telemetryRoutes.post('/plugins', registerPlugin);
37
+
38
+ export default telemetryRoutes;