@oas-tools/oas-telemetry 0.2.0 → 0.2.2

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 CHANGED
@@ -1,133 +1,133 @@
1
- # OAS TELEMETRY
2
-
3
- OAS Telemetry offers an express middleware designed for collecting telemetry data using Open Telemetry in applications built using the OpenAPI Specification (OAS). This middleware allows developers to easily incorporate telemetry functionality into their APIs.
4
-
5
- OAS Telemetry provides a set of endpoints that can be accessed to perform various actions related to telemetry data, such as starting and stopping data collection, resetting telemetry data, listing collected data, and searching for specific telemetry records. These endpoints can be easily integrated into an Express.js application, providing developers with a convenient way to manage and analyze telemetry data.
6
-
7
- Additionally, OAS Telemetry offers customization options, allowing developers to configure the telemetry middleware according to their specific requirements.
8
-
9
- Overall, OAS Telemetry will serve as a valuable tool for developers looking to gain insights into the operation and performance of their OAS-based APIs, enabling them to monitor, debug, and optimize their applications effectively.
10
-
11
-
12
-
13
-
14
-
15
- ## Usage
16
- To use the middelware add this two lines in your index.js (ESM):
17
- ```js
18
- import oasTelemetry from 'oas-telemetry';
19
- import {readFileSync} from 'fs';
20
-
21
- app.use(oasTelemetry({
22
- spec : readFileSync('./spec/oas.yaml',{ encoding: 'utf8', flag: 'r' })
23
- }))
24
-
25
- ```
26
-
27
- ## Custom Configuration
28
-
29
- You can also customize the telemetry configuration by passing options to the middleware function. For example:
30
- ```js
31
- const customTelemetryConfig = {
32
- exporter: myCustomExporter,
33
- spec: /* OAS content in json or yaml */
34
- };
35
-
36
- app.use(oasTelemetry(customTelemetryConfig));
37
- ```
38
-
39
- ## Telemetry UI
40
-
41
- You can access the telemetry UI in the endpoint ``/telemetry``
42
-
43
-
44
- ## API Telemetry Endpoints
45
-
46
- OAS Telemetry middleware adds the following endpoints to your Express application:
47
-
48
-
49
- - /telemetry/start: Start telemetry data collection.
50
- - /telemetry/stop: Stop telemetry data collection.
51
- - /telemetry/status: Get status of telemetry.
52
- - /telemetry/reset: Reset telemetry data.
53
- - /telemetry/list: List all telemetry data.
54
- - /telemetry/find (POST): Search telemetry data.
55
- - /telemetry/heapStats: Shows v8 heapStats.
56
-
57
-
58
- ## Simple Example [ES Module](https://nodejs.org/docs/latest/api/esm.html) (*.mjs)
59
- ```js index.mjs
60
- import oasTelemetry from '@oas-tools/oas-telemetry';
61
- import express from 'express';
62
-
63
- const app = express();
64
- const port = 3000;
65
-
66
- const spec = { "paths": {
67
- "/api/v1/pets": {
68
- "get": {
69
- "summary": "Get pets",
70
- "responses":{
71
- "200": {
72
- "description": "Success"
73
- }
74
- }
75
- }
76
- }
77
- }
78
- }
79
-
80
- app.use(oasTelemetry({
81
- spec : JSON.stringify(spec)
82
- }))
83
-
84
- app.use(express.json());
85
-
86
- app.get("/api/v1/pets", (req, res) => {
87
- res.send([{ name: "rocky"},{ name: "pikachu"}]);
88
- });
89
-
90
- app.listen(port, () => {
91
- console.log(`Example app listening at http://localhost:${port}`);
92
- console.log(`Telemetry portal available at http://localhost:${port}/telemetry`);
93
- });
94
- ```
95
-
96
- ## Simple Example [Common.js Module](https://nodejs.org/docs/latest/api/modules.html) (*.cjs)
97
- ```js index.cjs
98
- let oasTelemetry = require('@oas-tools/oas-telemetry');
99
- let express = require('express');
100
-
101
- const app = express();
102
- const port = 3000;
103
-
104
- const spec = { "paths": {
105
- "/api/v1/pets": {
106
- "get": {
107
- "summary": "Get pets",
108
- "responses":{
109
- "200": {
110
- "description": "Success"
111
- }
112
- }
113
- }
114
- }
115
- }
116
- }
117
-
118
- app.use(oasTelemetry({
119
- spec : JSON.stringify(spec)
120
- }))
121
-
122
- app.use(express.json());
123
-
124
- app.get("/api/v1/pets", (req, res) => {
125
- res.send([{ name: "rocky"},{ name: "pikachu"}]);
126
- });
127
-
128
- app.listen(port, () => {
129
- console.log(`Example app listening at http://localhost:${port}`);
130
- console.log(`Telemetry portal available at http://localhost:${port}/telemetry`);
131
- });
132
- ```
133
-
1
+ # OAS TELEMETRY
2
+
3
+ OAS Telemetry offers an express middleware designed for collecting telemetry data using Open Telemetry in applications built using the OpenAPI Specification (OAS). This middleware allows developers to easily incorporate telemetry functionality into their APIs.
4
+
5
+ OAS Telemetry provides a set of endpoints that can be accessed to perform various actions related to telemetry data, such as starting and stopping data collection, resetting telemetry data, listing collected data, and searching for specific telemetry records. These endpoints can be easily integrated into an Express.js application, providing developers with a convenient way to manage and analyze telemetry data.
6
+
7
+ Additionally, OAS Telemetry offers customization options, allowing developers to configure the telemetry middleware according to their specific requirements.
8
+
9
+ Overall, OAS Telemetry will serve as a valuable tool for developers looking to gain insights into the operation and performance of their OAS-based APIs, enabling them to monitor, debug, and optimize their applications effectively.
10
+
11
+
12
+
13
+
14
+
15
+ ## Usage
16
+ To use the middelware add this two lines in your index.js (ESM):
17
+ ```js
18
+ import oasTelemetry from 'oas-telemetry';
19
+ import {readFileSync} from 'fs';
20
+
21
+ app.use(oasTelemetry({
22
+ spec : readFileSync('./spec/oas.yaml',{ encoding: 'utf8', flag: 'r' })
23
+ }))
24
+
25
+ ```
26
+
27
+ ## Custom Configuration
28
+
29
+ You can also customize the telemetry configuration by passing options to the middleware function. For example:
30
+ ```js
31
+ const customTelemetryConfig = {
32
+ exporter: myCustomExporter,
33
+ spec: /* OAS content in json or yaml */
34
+ };
35
+
36
+ app.use(oasTelemetry(customTelemetryConfig));
37
+ ```
38
+
39
+ ## Telemetry UI
40
+
41
+ You can access the telemetry UI in the endpoint ``/telemetry``
42
+
43
+
44
+ ## API Telemetry Endpoints
45
+
46
+ OAS Telemetry middleware adds the following endpoints to your Express application:
47
+
48
+
49
+ - /telemetry/start: Start telemetry data collection.
50
+ - /telemetry/stop: Stop telemetry data collection.
51
+ - /telemetry/status: Get status of telemetry.
52
+ - /telemetry/reset: Reset telemetry data.
53
+ - /telemetry/list: List all telemetry data.
54
+ - /telemetry/find (POST): Search telemetry data.
55
+ - /telemetry/heapStats: Shows v8 heapStats.
56
+
57
+
58
+ ## Simple Example [ES Module](https://nodejs.org/docs/latest/api/esm.html) (*.mjs)
59
+ ```js index.mjs
60
+ import oasTelemetry from '@oas-tools/oas-telemetry';
61
+ import express from 'express';
62
+
63
+ const app = express();
64
+ const port = 3000;
65
+
66
+ const spec = { "paths": {
67
+ "/api/v1/pets": {
68
+ "get": {
69
+ "summary": "Get pets",
70
+ "responses":{
71
+ "200": {
72
+ "description": "Success"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ app.use(oasTelemetry({
81
+ spec : JSON.stringify(spec)
82
+ }))
83
+
84
+ app.use(express.json());
85
+
86
+ app.get("/api/v1/pets", (req, res) => {
87
+ res.send([{ name: "rocky"},{ name: "pikachu"}]);
88
+ });
89
+
90
+ app.listen(port, () => {
91
+ console.log(`Example app listening at http://localhost:${port}`);
92
+ console.log(`Telemetry portal available at http://localhost:${port}/telemetry`);
93
+ });
94
+ ```
95
+
96
+ ## Simple Example [Common.js Module](https://nodejs.org/docs/latest/api/modules.html) (*.cjs)
97
+ ```js index.cjs
98
+ let oasTelemetry = require('@oas-tools/oas-telemetry');
99
+ let express = require('express');
100
+
101
+ const app = express();
102
+ const port = 3000;
103
+
104
+ const spec = { "paths": {
105
+ "/api/v1/pets": {
106
+ "get": {
107
+ "summary": "Get pets",
108
+ "responses":{
109
+ "200": {
110
+ "description": "Success"
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ app.use(oasTelemetry({
119
+ spec : JSON.stringify(spec)
120
+ }))
121
+
122
+ app.use(express.json());
123
+
124
+ app.get("/api/v1/pets", (req, res) => {
125
+ res.send([{ name: "rocky"},{ name: "pikachu"}]);
126
+ });
127
+
128
+ app.listen(port, () => {
129
+ console.log(`Example app listening at http://localhost:${port}`);
130
+ console.log(`Telemetry portal available at http://localhost:${port}/telemetry`);
131
+ });
132
+ ```
133
+
@@ -7,21 +7,36 @@ exports.InMemoryExporter = void 0;
7
7
  var _core = require("@opentelemetry/core");
8
8
  var _nedb = _interopRequireDefault(require("nedb"));
9
9
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
11
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
12
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
13
+ let dbglog = () => {};
14
+ if (process.env.OTDEBUG == "true") dbglog = console.log;
15
+
10
16
  //import in memory database
11
17
 
12
18
  class InMemoryExporter {
13
19
  constructor() {
14
20
  this._spans = new _nedb.default();
15
- this._stopped = false;
21
+ this._stopped = true;
16
22
  }
17
23
  export(readableSpans, resultCallback) {
18
24
  try {
19
25
  if (!this._stopped) {
20
- // Remove circular references
21
- const cleanSpans = readableSpans.map(span => removeCircular(span));
26
+ // Prepare spans to be inserted into the in-memory database (remove circular references and convert to nested objects)
27
+ const cleanSpans = readableSpans.map(nestedSpan => removeCircularRefs(nestedSpan)) // to avoid JSON parsing error
28
+ .map(span => applyNesting(span)) // to avoid dot notation in keys (neDB does not support dot notation in keys)
29
+ .filter(span => !span.attributes?.http?.target?.includes("/telemetry")); // to avoid telemetry spans
22
30
 
23
31
  // Insert spans into the in-memory database
24
32
  this._spans.insert(cleanSpans, (err, newDoc) => {
33
+ InMemoryExporter.plugins.forEach((p, i) => {
34
+ cleanSpans.forEach(t => {
35
+ dbglog(`Sending trace <${t._id}> to plugin (Plugin #${i}) <${p.name}>`);
36
+ dbglog(`Trace: \n<${JSON.stringify(t, null, 2)}`);
37
+ p.newTrace(t);
38
+ });
39
+ });
25
40
  if (err) {
26
41
  console.error(err);
27
42
  return;
@@ -50,8 +65,8 @@ class InMemoryExporter {
50
65
  this._spans = new _nedb.default();
51
66
  return this.forceFlush();
52
67
  }
53
- /**
54
- * Exports any pending spans in the exporter
68
+ /**
69
+ * Exports any pending spans in the exporter
55
70
  */
56
71
  forceFlush() {
57
72
  return Promise.resolve();
@@ -62,9 +77,15 @@ class InMemoryExporter {
62
77
  getFinishedSpans() {
63
78
  return this._spans;
64
79
  }
80
+ activatePlugin(plugin) {
81
+ dbglog(`Activating plugin <${plugin.getName()}>...`);
82
+ InMemoryExporter.plugins.push(plugin);
83
+ dbglog(`Plugin <${plugin.getName()}> active (Total active plugins: ${InMemoryExporter.plugins.length})`);
84
+ }
65
85
  }
66
86
  exports.InMemoryExporter = InMemoryExporter;
67
- function removeCircular(obj) {
87
+ _defineProperty(InMemoryExporter, "plugins", []);
88
+ function removeCircularRefs(obj) {
68
89
  const seen = new WeakMap(); // Used to keep track of visited objects
69
90
 
70
91
  // Replacer function to handle circular references
@@ -86,10 +107,68 @@ function removeCircular(obj) {
86
107
  // Convert the object to a string and then parse it back
87
108
  // This will trigger the replacer function to handle circular references
88
109
  const jsonString = JSON.stringify(obj, replacer);
89
- const spanNoDotsInKeys = jsonString.replace(/[^"]*":/g, match => {
90
- // Replace all dots in the key with underscores (e.g. "http.method" -> "http_method")
91
- const newMatch = match.replace(/\./g, "_dot_");
92
- return newMatch;
93
- });
94
- return JSON.parse(spanNoDotsInKeys);
110
+ return JSON.parse(jsonString);
111
+ }
112
+
113
+ /**
114
+ * Recursively converts dot-separated keys in an object to nested objects.
115
+ *
116
+ * @param {Object} obj - The object to process.
117
+ * @returns {Object} - The object with all dot-separated keys converted to nested objects.
118
+ * @example
119
+ * // Input:
120
+ * // {
121
+ * // "http.method": "GET",
122
+ * // "http.url": "http://example.com",
123
+ * // "nested.obj.key": "value"
124
+ * // }
125
+ * // Output:
126
+ * // {
127
+ * // "http": {
128
+ * // "method": "GET",
129
+ * // "url": "http://example.com"
130
+ * // },
131
+ * // "nested": {
132
+ * // "obj": {
133
+ * // "key": "value"
134
+ * // }
135
+ * // }
136
+ * // }
137
+ */
138
+ function convertToNestedObject(obj) {
139
+ const result = {};
140
+ for (const key in obj) {
141
+ const keys = key.split('.');
142
+ let temp = result;
143
+ for (let i = 0; i < keys.length; i++) {
144
+ const currentKey = keys[i];
145
+ if (i === keys.length - 1) {
146
+ // Last key, set the value
147
+ temp[currentKey] = obj[key];
148
+ } else {
149
+ // Intermediate key, ensure the object exists
150
+ if (!temp[currentKey]) {
151
+ temp[currentKey] = {};
152
+ }
153
+ temp = temp[currentKey];
154
+ }
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+
160
+ /**
161
+ * Applies nesting to all dot-separated keys within an object.
162
+ *
163
+ * @param {Object} obj - The object to apply nesting to.
164
+ * @returns {Object} - The transformed object with nested structures.
165
+ */
166
+ function applyNesting(obj) {
167
+ // Recursively apply convertToNestedObject to each level of the object
168
+ for (const key in obj) {
169
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
170
+ obj[key] = applyNesting(obj[key]);
171
+ }
172
+ }
173
+ return convertToNestedObject(obj);
95
174
  }
package/dist/index.cjs CHANGED
@@ -10,11 +10,14 @@ var _v = _interopRequireDefault(require("v8"));
10
10
  var _fs = require("fs");
11
11
  var _path = _interopRequireDefault(require("path"));
12
12
  var _jsYaml = _interopRequireDefault(require("js-yaml"));
13
- var _url = require("url");
14
13
  var _ui = _interopRequireDefault(require("./ui.cjs"));
14
+ var _axios = _interopRequireDefault(require("axios"));
15
15
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
16
  // telemetryMiddleware.js
17
17
 
18
+ let dbglog = () => {};
19
+ if (process.env.OTDEBUG == "true") dbglog = console.log;
20
+ let plugins = [];
18
21
  let telemetryStatus = {
19
22
  active: true
20
23
  };
@@ -25,19 +28,18 @@ let telemetryConfig = {
25
28
  };
26
29
  function oasTelemetry(tlConfig) {
27
30
  if (tlConfig) {
28
- console.log('Telemetry config provided');
31
+ dbglog('Telemetry config provided');
29
32
  telemetryConfig = tlConfig;
30
33
  if (telemetryConfig.exporter == undefined) telemetryConfig.exporter = _telemetry.inMemoryExporter;
31
34
  }
32
- if (telemetryConfig.spec) console.log(`Spec content provided`);else {
33
- if (telemetryConfig.specFileName != "") console.log(`Spec file used for telemetry: ${telemetryConfig.specFileName}`);else {
34
- console.log("No spec available !");
35
+ if (telemetryConfig.spec) dbglog(`Spec content provided`);else {
36
+ if (telemetryConfig.specFileName != "") dbglog(`Spec file used for telemetry: ${telemetryConfig.specFileName}`);else {
37
+ console.error("No spec available !");
35
38
  }
36
39
  }
37
40
  const router = (0, _express.Router)();
38
-
39
- //const baseURL = telemetryConfig.baseURL;
40
-
41
+ if (telemetryConfig.baseURL) baseURL = telemetryConfig.baseURL;
42
+ router.use((0, _express.json)());
41
43
  router.get(baseURL, mainPage);
42
44
  router.get(baseURL + "/detail/*", detailPage);
43
45
  router.get(baseURL + "/spec", specLoader);
@@ -49,6 +51,8 @@ function oasTelemetry(tlConfig) {
49
51
  router.get(baseURL + "/list", listTelemetry);
50
52
  router.post(baseURL + "/find", findTelemetry);
51
53
  router.get(baseURL + "/heapStats", heapStats);
54
+ router.get(baseURL + "/plugins", listPlugins);
55
+ router.post(baseURL + "/plugins", registerPlugin);
52
56
  return router;
53
57
  }
54
58
  const apiPage = (req, res) => {
@@ -87,7 +91,7 @@ const specLoader = (req, res) => {
87
91
  res.setHeader('Content-Type', 'application/json');
88
92
  res.send(json);
89
93
  } catch (e) {
90
- console.log(`ERROR loading spec file ${telemetryConfig.specFileName}: ${e}`);
94
+ console.error(`ERROR loading spec file ${telemetryConfig.specFileName}: ${e}`);
91
95
  }
92
96
  } else {
93
97
  if (telemetryConfig.spec) {
@@ -98,7 +102,7 @@ const specLoader = (req, res) => {
98
102
  try {
99
103
  spec = JSON.stringify(_jsYaml.default.load(telemetryConfig.spec), null, 2);
100
104
  } catch (ey) {
101
- console.log(`Error parsing spec: ${ej} - ${ey}`);
105
+ console.error(`Error parsing spec: ${ej} - ${ey}`);
102
106
  }
103
107
  }
104
108
  if (!spec) {
@@ -112,16 +116,17 @@ const specLoader = (req, res) => {
112
116
  };
113
117
  const startTelemetry = (req, res) => {
114
118
  telemetryConfig.exporter.start();
115
- telemetryStatus.active = true;
116
119
  res.send('Telemetry started');
117
120
  };
118
121
  const stopTelemetry = (req, res) => {
119
122
  telemetryConfig.exporter.stop();
120
- telemetryStatus.active = false;
121
123
  res.send('Telemetry stopped');
122
124
  };
123
125
  const statusTelemetry = (req, res) => {
124
- res.send(telemetryStatus);
126
+ const status = !telemetryConfig.exporter._stopped || false;
127
+ res.send({
128
+ active: status
129
+ });
125
130
  };
126
131
  const resetTelemetry = (req, res) => {
127
132
  telemetryConfig.exporter.reset();
@@ -141,25 +146,6 @@ const listTelemetry = (req, res) => {
141
146
  });
142
147
  });
143
148
  };
144
- const findTelemetry = (req, res) => {
145
- const spansDB = telemetryConfig.exporter.getFinishedSpans();
146
- const search = req.body;
147
- spansDB.find(search, (err, docs) => {
148
- if (err) {
149
- console.error(err);
150
- res.send({
151
- spansCount: "error",
152
- spans: []
153
- });
154
- return;
155
- }
156
- const spans = docs;
157
- res.send({
158
- spansCount: spans.length,
159
- spans: spans
160
- });
161
- });
162
- };
163
149
  const heapStats = (req, res) => {
164
150
  var heapStats = _v.default.getHeapStatistics();
165
151
 
@@ -171,4 +157,74 @@ const heapStats = (req, res) => {
171
157
  roundedHeapStats['units'] = 'MB';
172
158
  res.send(roundedHeapStats);
173
159
  };
160
+ const findTelemetry = (req, res) => {
161
+ const spansDB = telemetryConfig.exporter.getFinishedSpans();
162
+ const body = req.body;
163
+ const search = body?.search ? body.search : {};
164
+ if (body?.flags?.containsRegex) {
165
+ try {
166
+ body.config?.regexIds?.forEach(regexId => {
167
+ search[regexId] = new RegExp(search[regexId]);
168
+ });
169
+ } catch (e) {
170
+ console.error(e);
171
+ res.status(404).send({
172
+ spansCount: 0,
173
+ spans: [],
174
+ error: e
175
+ });
176
+ return;
177
+ }
178
+ spansDB.find(search, (err, docs) => {
179
+ if (err) {
180
+ console.error(err);
181
+ res.status(404).send({
182
+ spansCount: 0,
183
+ spans: [],
184
+ error: err
185
+ });
186
+ return;
187
+ }
188
+ const spans = docs;
189
+ res.send({
190
+ spansCount: spans.length,
191
+ spans: spans
192
+ });
193
+ });
194
+ }
195
+ };
196
+ const listPlugins = (req, res) => {
197
+ res.send(plugins.map(p => {
198
+ return {
199
+ id: p.id,
200
+ url: p.url,
201
+ active: p.active
202
+ };
203
+ }));
204
+ };
205
+ const registerPlugin = (req, res) => {
206
+ let pluginResource = req.body;
207
+ dbglog(`Plugin Registration Request: = ${JSON.stringify(req.body, null, 2)}...`);
208
+ dbglog(`Getting plugin at ${pluginResource.url}...`);
209
+ _axios.default.get(pluginResource.url).then(response => {
210
+ dbglog(`Plugin fetched.`);
211
+ const pluginCode = response.data;
212
+ dbglog("Plugin size: " + pluginCode.length);
213
+ var plugin;
214
+ eval(pluginCode);
215
+ plugin.load(pluginResource.config);
216
+ if (plugin.isConfigured()) {
217
+ dbglog(`Loaded plugin <${plugin.getName()}>`);
218
+ pluginResource.plugin = plugin;
219
+ pluginResource.name = plugin.getName();
220
+ pluginResource.active = true;
221
+ plugins.push(pluginResource);
222
+ _telemetry.inMemoryExporter.activatePlugin(pluginResource.plugin);
223
+ res.status(201).send(`Plugin registered`);
224
+ } else {
225
+ console.error(`Plugin <${plugin.getName()}> can not be configured`);
226
+ res.status(400).send(`Plugin configuration problem`);
227
+ }
228
+ }).catch(err => console.error("registerPlugin:" + err));
229
+ };
174
230
  module.exports = exports.default;