@oas-tools/oas-telemetry 0.4.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 -271
  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 +0 -0
  20. package/dist/types/index.d.ts +0 -0
  21. package/dist/types/telemetry.d.ts +0 -0
  22. package/dist/types/ui.d.ts +0 -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 -310
  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
@@ -0,0 +1,1520 @@
1
+
2
+
3
+ // WARNING: This file is autogenerated. DO NOT EDIT!
4
+ //This file is autogenerated by dev/ui/exportHtmlToUi.js
5
+ const ui = (baseURL) => {
6
+ if(!baseURL) return htmlMap;
7
+ return Object.keys(htmlMap).reduce((acc, key) => {
8
+ acc[key] = htmlMap[key].replace(/\/telemetry/g, baseURL);
9
+ return acc;
10
+ }, {});
11
+ }
12
+
13
+ export const htmlMap =
14
+ {
15
+ detail: `<!DOCTYPE html>
16
+ <html lang="en">
17
+
18
+ <head>
19
+ <meta charset="UTF-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
+ <title>OAS - Telemetry</title>
22
+ <style>
23
+ .box {
24
+ width: 100%;
25
+ margin: 0 auto;
26
+ background: rgba(255, 255, 255, 0.2);
27
+ padding: 35px;
28
+ border: 2px solid #fff;
29
+ border-radius: 20px/50px;
30
+ background-clip: padding-box;
31
+ text-align: center;
32
+ }
33
+
34
+ .overlay {
35
+ position: fixed;
36
+ top: 0;
37
+ bottom: 0;
38
+ left: 0;
39
+ right: 0;
40
+ background: rgba(0, 0, 0, 0.7);
41
+ transition: opacity 500ms;
42
+ visibility: hidden;
43
+ opacity: 0;
44
+ overflow: scroll;
45
+ }
46
+
47
+ .overlay:target {
48
+ visibility: visible;
49
+ opacity: 1;
50
+ }
51
+
52
+ .popup {
53
+ margin: 70px auto;
54
+ padding: 20px;
55
+ background: #fff;
56
+ border-radius: 5px;
57
+ width: 70%;
58
+ position: relative;
59
+ transition: all 5s ease-in-out;
60
+ font-size: small;
61
+ }
62
+
63
+ .popup .close {
64
+ position: absolute;
65
+ top: 20px;
66
+ right: 30px;
67
+ transition: all 200ms;
68
+ font-size: 30px;
69
+ font-weight: bold;
70
+ text-decoration: none;
71
+ color: #333;
72
+ }
73
+ </style>
74
+ <style>
75
+ body {
76
+ font-family: Arial, sans-serif;
77
+ margin: 0;
78
+ padding: 0;
79
+ background-color: #f5f5f5;
80
+ }
81
+
82
+ .header {
83
+ background-color: #333;
84
+ color: #fff;
85
+ padding: 20px;
86
+ text-align: left;
87
+ display: flex;
88
+ justify-content: space-between;
89
+ }
90
+
91
+ .header h1 {
92
+ display: inline-block;
93
+ margin: 0;
94
+ font-size: 24px;
95
+ }
96
+
97
+ .header .links {
98
+ display: flex;
99
+ align-items: center;
100
+ }
101
+
102
+ .links a {
103
+ color: #fff;
104
+ text-decoration: none;
105
+ margin-left: 30px;
106
+ }
107
+
108
+ .page {
109
+ margin: 0;
110
+ padding: 0;
111
+ }
112
+
113
+ .panel-conainer {
114
+ min-width: 60%;
115
+ width: fit-content;
116
+ margin: 20px auto;
117
+ }
118
+
119
+ .panel {
120
+ background-color: #fff;
121
+ border: 1px solid #ddd;
122
+ margin: 10px;
123
+ border-radius: 4px;
124
+ }
125
+
126
+ .panel-header {
127
+ background-color: #f1f1f1;
128
+ padding: 10px;
129
+ font-weight: bold;
130
+ cursor: pointer;
131
+ }
132
+
133
+ .panel-content {
134
+ display: none;
135
+ padding: 15px;
136
+
137
+ /* items margin */
138
+ >* {
139
+ margin: 5px 0;
140
+ }
141
+
142
+ }
143
+
144
+ .panel.open .panel-content {
145
+ display: flex;
146
+ flex-direction: column;
147
+ padding: 15px;
148
+ justify-content: center;
149
+ align-items: center;
150
+
151
+ }
152
+
153
+
154
+ button {
155
+ background-color: #007bff;
156
+ color: white;
157
+ border: none;
158
+ padding: 10px 15px;
159
+ cursor: pointer;
160
+ border-radius: 4px;
161
+ }
162
+
163
+ button:hover {
164
+ background-color: #0056b3;
165
+ }
166
+
167
+ table {
168
+ width: fit-content;
169
+ border-collapse: collapse;
170
+ margin: 100%;
171
+ }
172
+
173
+ th,
174
+ td {
175
+ border: 1px solid #dddddd;
176
+ padding: 8px;
177
+ text-align: left;
178
+ }
179
+
180
+ th {
181
+ background-color: #f2f2f2;
182
+ }
183
+
184
+
185
+
186
+ .spaced-row {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: space-between;
190
+ width: 100%;
191
+ }
192
+
193
+ .row {
194
+ display: flex;
195
+ align-items: center;
196
+ }
197
+ </style>
198
+ <script>
199
+ async function checkTelemetryStatus() {
200
+ try {
201
+ const response = await fetch('/telemetry/check');
202
+ const data = await response.json();
203
+ if (!data.valid) {
204
+ window.location.href = '/telemetry/login';
205
+ }
206
+ } catch (error) {
207
+ console.error('Error checking telemetry status:', error);
208
+ }
209
+ }
210
+
211
+ setInterval(checkTelemetryStatus, 5000); // Check every 5 seconds
212
+ </script>
213
+ </head>
214
+
215
+ <body>
216
+ <div class="header">
217
+ <h1>OAS-Telemetry</h1>
218
+ <div class="links">
219
+ <a href="/telemetry/">Back</a>
220
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">Documentation</a>
221
+ <a target="_blank" href="https://www.npmjs.com/package/@oas-tools/oas-telemetry">NPM</a>
222
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">GitHub</a>
223
+ <script>
224
+ // Check if the user is logged in
225
+ fetch('/telemetry/check')
226
+ .then(response => response.json())
227
+ .then(data => {
228
+ if (data.valid) {
229
+ const logoutLink = document.createElement('a');
230
+ logoutLink.href = '/telemetry/logout';
231
+ logoutLink.textContent = 'Logout';
232
+ document.querySelector('.links').appendChild(logoutLink);
233
+ }
234
+ })
235
+ .catch(error => console.error('Error:', error));
236
+ </script>
237
+ </div>
238
+ </div>
239
+ <div class="page">
240
+ <div class="panel-conainer">
241
+ <!-- Endpoint Info Panel -->
242
+ <div class="panel open" id="panel1">
243
+ <div class="panel-header" onclick="togglePanel('panel1')">Endpoint Information</div>
244
+ <div class="panel-content">
245
+ <h2><span id="heading">Telemetry spans for...</span></h2>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Traces Panel -->
250
+ <div class="panel open" id="panel2">
251
+ <div class="panel-header" onclick="togglePanel('panel2')">Request Traces</div>
252
+ <div class="panel-content">
253
+ <table id="apiTable">
254
+ <thead>
255
+ <tr>
256
+ <th onclick="sortTable(0)">TimeStamp</th>
257
+ <th onclick="sortTable(1)">End point</th>
258
+ <th onclick="sortTable(2)">Method</th>
259
+ <th onclick="sortTable(3)">Status</th>
260
+ <th onclick="sortTable(4)" style="text-align: center;">Response time<br> (sec)</th>
261
+ </tr>
262
+ </thead>
263
+ <tbody>
264
+ </tbody>
265
+ </table>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+
271
+ <!-- Popup overlay -->
272
+ <div id="popupOverlay" class="overlay">
273
+ <div class="popup">
274
+ <pre id="tracePopup">
275
+ "attributes": {
276
+ "http": {
277
+ "url": "http://localhost:3000/api/v1/test",
278
+ "host": "localhost:3000",
279
+ "method": "GET",
280
+ "scheme": "http",
281
+ "target": "/api/v1/test",
282
+ "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
283
+ "flavor": "1.1",
284
+ "status_code": 304,
285
+ "status_text": "NOT MODIFIED"
286
+ },
287
+ "net": {
288
+ "host": {
289
+ "name": "localhost",
290
+ "ip": "::1",
291
+ "port": 3000
292
+ },
293
+ "transport": "ip_tcp",
294
+ "peer": {
295
+ "ip": "::1",
296
+ "port": 58101
297
+ }
298
+ }
299
+ },
300
+ "links": {
301
+
302
+ },
303
+ "events": {
304
+
305
+ },
306
+ "_droppedAttributesCount": 0,
307
+ "_droppedEventsCount": 0,
308
+ "_droppedLinksCount": 0,
309
+ "status": {
310
+ "code": 0
311
+ },
312
+ "endTime": {
313
+ "0": 1724019619,
314
+ "1": 414126300
315
+ },
316
+ "_ended": true,
317
+ "_duration": {
318
+ "0": 0,
319
+ "1": 2126300
320
+ },
321
+ "name": "GET",
322
+ "_spanContext": {
323
+ "traceId": "ee70c9a937bbf95940a8971dc96077b3",
324
+ "spanId": "4fe34ee075253ecb",
325
+ "traceFlags": 1
326
+ },
327
+ "kind": 1,
328
+ "_performanceStartTime": 40561.097599983215,
329
+ "_performanceOffset": -8.425537109375,
330
+ "_startTimeProvided": false,
331
+ "startTime": {
332
+ "0": 1724019619,
333
+ "1": 412000000
334
+ },
335
+ "resource": {
336
+ "_attributes": {
337
+ "service": {
338
+ "name": "unknown_service:node"
339
+ },
340
+ "telemetry": {
341
+ "sdk": {
342
+ "language": "nodejs",
343
+ "name": "opentelemetry",
344
+ "version": "1.22.0"
345
+ }
346
+ },
347
+ "process": {
348
+ "pid": 23128,
349
+ "executable": {
350
+ "name": "npm",
351
+ "path": "C:\\Program Files\\nodejs\\node.exe"
352
+ },
353
+ "command_args": {
354
+ "0": "C:\\Program Files\\nodejs\\node.exe",
355
+ "1": "C:\\Personal\\ISA\\telemetry\\ot-ui-poc\\index"
356
+ },
357
+ "runtime": {
358
+ "version": "14.21.3",
359
+ "name": "nodejs",
360
+ "description": "Node.js"
361
+ },
362
+ "command": "C:\\Personal\\ISA\\telemetry\\ot-ui-poc\\index",
363
+ "owner": "manol"
364
+ }
365
+ },
366
+ "asyncAttributesPending": false,
367
+ "_syncAttributes": {
368
+ "service": {
369
+ "name": "unknown_service:node"
370
+ },
371
+ "telemetry": {
372
+ "sdk": {
373
+ "language": "nodejs",
374
+ "name": "opentelemetry",
375
+ "version": "1.22.0"
376
+ }
377
+ }
378
+ },
379
+ "_asyncAttributesPromise": {
380
+
381
+ }
382
+ },
383
+ "instrumentationLibrary": {
384
+ "name": "@opentelemetry/instrumentation-http",
385
+ "version": "0.51.0"
386
+ },
387
+ "_spanLimits": {
388
+ "attributeValueLengthLimit": null,
389
+ "attributeCountLimit": 128,
390
+ "linkCountLimit": 128,
391
+ "eventCountLimit": 128,
392
+ "attributePerEventCountLimit": 128,
393
+ "attributePerLinkCountLimit": 128
394
+ },
395
+ "_attributeValueLengthLimit": null,
396
+ "_spanProcessor": "oas-telemetry skips this field to avoid circular reference",
397
+ "_id": "f2989F2IDm3uSfml"
398
+ </pre>
399
+ <a class="close" href="#" onclick="hidePopup()">&times;</a>
400
+ </div>
401
+ </div>
402
+
403
+ <script>
404
+ let traceCount = -1;
405
+ let LOG = true;
406
+
407
+ function log(s) {
408
+ if (LOG)
409
+ console.log(s);
410
+ }
411
+
412
+ function parsePath() {
413
+
414
+ let detailPath = window.location.pathname.split("/");
415
+
416
+ if (detailPath.length < 6 || detailPath[5] == "") {
417
+ alert("Wrong invocation params");
418
+ return;
419
+ }
420
+
421
+ let status = parseInt(detailPath[3]);
422
+
423
+ if (Number.isNaN(status))
424
+ status = -1;
425
+
426
+ const method = detailPath[4];
427
+
428
+
429
+ const path = decodeURI("/" + detailPath
430
+ .splice(5, detailPath.length - 5)
431
+ .filter(c => (c != ""))
432
+ .join("/"));
433
+
434
+ headingObj = document.getElementById('heading');
435
+ headingObj.textContent = \`Telemetry for \${path} - \${method} - \${status} \`;
436
+ fetchTracesByFind(path, method, status);
437
+ }
438
+
439
+
440
+ async function fetchTracesByFind(path, method, status) {
441
+ try {
442
+ let statusOr = [{ "attributes.http.status_code": parseInt(status) }]
443
+ if (status == "200") {
444
+ statusOr.push({ "attributes.http.status_code": 304 }); //Some servers return 304 instead of 200
445
+ }
446
+ log(\`Fetching traces for <\${path}> - \${method} - \${status} \`);
447
+ const body = {
448
+ "flags": { "containsRegex": true },
449
+ "config": { "regexIds": ["attributes.http.target", "attributes.http.status_code"] },
450
+ "search": {
451
+ "attributes.http.target": getPathRegEx(path),
452
+ "attributes.http.method": method.toUpperCase(),
453
+ "\$or": statusOr
454
+ }
455
+ };
456
+ log("body: " + JSON.stringify(body, null, 2));
457
+ //response is to the post at /telemetry/find
458
+ const response = await fetch("/telemetry/find", {
459
+ method: "POST",
460
+ headers: {
461
+ "Content-Type": "application/json"
462
+ },
463
+ body: JSON.stringify(body)
464
+ });
465
+
466
+ if (!response.ok) {
467
+ throw new Error("ERROR getting the Traces.");
468
+ }
469
+ response.json().then(data => {
470
+ //Data should be {spansCount: 0, spans: []}
471
+ log("Data: " + JSON.stringify(data, null, 2));
472
+ if (data && data.spans) {
473
+ loadTraces(data.spans);
474
+ }
475
+ });
476
+
477
+ } catch (error) {
478
+ console.error("ERROR getting the Traces :", error);
479
+ }
480
+ }
481
+
482
+ function getPathRegEx(path) {
483
+ let pathComponents = path.split("/");
484
+ let pathRegExpStr = "^"
485
+
486
+ pathComponents.forEach(c => {
487
+ if (c != "") {
488
+ pathRegExpStr += "/";
489
+ if (c.charAt(0) == "{" && c.charAt(c.length - 1) == "}") {
490
+ // Ensure it matches at least one character (.+)
491
+ pathRegExpStr += "(.+)";
492
+ } else {
493
+ pathRegExpStr += c;
494
+ }
495
+ }
496
+ });
497
+
498
+ // Allow an optional trailing slash
499
+ pathRegExpStr += "/?\$";
500
+
501
+ return pathRegExpStr;
502
+ }
503
+
504
+
505
+ function calculateTiming(startSecInput, startNanoSecInput, endSecInput, endNanoSecInput, precision = 3) {
506
+ // Convert inputs to numbers
507
+ let startSec = parseInt(startSecInput);
508
+ let startNanoSec = parseInt(startNanoSecInput);
509
+ let endSec = parseInt(endSecInput);
510
+ let endNanoSec = parseInt(endNanoSecInput);
511
+
512
+ // Convert nanoseconds to fractional seconds and add to seconds
513
+ let preciseStart = startSec + startNanoSec / 1e9; // Nanoseconds to seconds
514
+ let preciseEnd = endSec + endNanoSec / 1e9; // Nanoseconds to seconds
515
+
516
+ // Calculate duration
517
+ let preciseDuration = preciseEnd - preciseStart;
518
+
519
+ // Create Date objects and ISO timestamps
520
+ let startDate = new Date(preciseStart * 1000);
521
+ let endDate = new Date(preciseEnd * 1000);
522
+
523
+ return {
524
+ preciseStart: preciseStart, // Precise start time in seconds
525
+ preciseEnd: preciseEnd, // Precise end time in seconds
526
+ preciseDuration: preciseDuration, // Duration in seconds
527
+ start: parseFloat(preciseStart.toFixed(precision)), // Rounded start time
528
+ end: parseFloat(preciseEnd.toFixed(precision)), // Rounded end time
529
+ duration: parseFloat(preciseDuration.toFixed(precision)), // Rounded duration
530
+ startDate: startDate, // Date object for start time
531
+ endDate: endDate, // Date object for end time
532
+ startTS: startDate.toISOString(), // ISO timestamp for start
533
+ endTS: endDate.toISOString() // ISO timestamp for end
534
+ };
535
+ }
536
+
537
+ function parseTraceInfo(t) {
538
+ const ep = t.attributes.http.target;
539
+ const method = t.attributes.http.method.toLowerCase();
540
+ const status = t.attributes.http.status_code;
541
+
542
+ const timing = calculateTiming(t.startTime[0], t.startTime[1], t.endTime[0], t.endTime[1]);
543
+
544
+ log(\`\${timing.startTS} - \${timing.endTS} - \${t._spanContext.traceId} - \${t.name} - \${ep} - \${status} - \${timing.duration}\`);
545
+ return {
546
+ ts: timing.startTS,
547
+ ep: ep,
548
+ method: method,
549
+ status: status,
550
+ duration: timing.duration
551
+ };
552
+ }
553
+
554
+ function loadTraces(traces) {
555
+
556
+ const tableBody = document.getElementById('apiTable').getElementsByTagName('tbody')[0];
557
+ while (tableBody.hasChildNodes()) {
558
+ tableBody.removeChild(tableBody.lastChild);
559
+ }
560
+
561
+ traces.forEach(trace => {
562
+ const row = tableBody.insertRow();
563
+ const cellTS = row.insertCell(0);
564
+ const cellEP = row.insertCell(1);
565
+ const cellMethod = row.insertCell(2);
566
+ cellMethod.style.textAlign = "center";
567
+ const cellStatus = row.insertCell(3);
568
+ cellStatus.style.textAlign = "center";
569
+ const cellDuration = row.insertCell(4);
570
+ cellDuration.style.textAlign = "center";
571
+
572
+ let t = parseTraceInfo(trace);
573
+
574
+ cellTS.textContent = t.ts;
575
+ cellEP.textContent = t.ep;
576
+ cellMethod.textContent = t.method;
577
+ cellStatus.textContent = t.status;
578
+ cellDuration.textContent = t.duration.toFixed(3);
579
+
580
+ row.trace = trace;
581
+ row.onclick = function () {
582
+ const popup = document.getElementById("tracePopup");
583
+ popup.firstChild.nodeValue = JSON.stringify(this.trace, null, 2);
584
+ const popupOverlay = document.getElementById("popupOverlay");
585
+ popupOverlay.style.visibility = "visible";
586
+ popupOverlay.style.opacity = 1;
587
+ };
588
+ });
589
+ }
590
+
591
+ function sortTable(column) {
592
+ const table = document.getElementById('apiTable');
593
+ let rows, switching, i, x, y, shouldSwitch;
594
+ switching = true;
595
+ // Loop until no switching has been done:
596
+ while (switching) {
597
+ switching = false;
598
+ rows = table.rows;
599
+ // Loop through all table rows (except the first, which contains table headers):
600
+ for (i = 1; i < (rows.length - 1); i++) {
601
+ shouldSwitch = false;
602
+ // Get the two elements you want to compare, one from current row and one from the next:
603
+ x = rows[i].getElementsByTagName("TD")[column];
604
+ y = rows[i + 1].getElementsByTagName("TD")[column];
605
+ // Check if the two rows should switch place:
606
+ if (x.textContent.toLowerCase() > y.textContent.toLowerCase()) {
607
+ shouldSwitch = true;
608
+ break;
609
+ }
610
+ }
611
+ if (shouldSwitch) {
612
+ // If a switch has been marked, make the switch and mark that a switch has been done:
613
+ rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
614
+ switching = true;
615
+ }
616
+ }
617
+ }
618
+
619
+ function hidePopup() {
620
+ const popupOverlay = document.getElementById("popupOverlay");
621
+ popupOverlay.style.visibility = "hidden";
622
+ popupOverlay.style.opacity = 0;
623
+ }
624
+
625
+ function togglePanel(panelId) {
626
+ const panel = document.getElementById(panelId);
627
+ panel.classList.toggle('open');
628
+ }
629
+
630
+ window.onload = parsePath();
631
+ </script>
632
+
633
+ </body>
634
+
635
+ </html>`,
636
+ login: `<!DOCTYPE html>
637
+ <html lang="en">
638
+ <head>
639
+ <meta charset="UTF-8">
640
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
641
+ <title>Telemetry Login</title>
642
+ <style>
643
+ body {
644
+ font-family: Arial, sans-serif;
645
+ margin: 0;
646
+ padding: 0;
647
+ background-color: #f5f5f5;
648
+ }
649
+
650
+ .header {
651
+ background-color: #333;
652
+ color: #fff;
653
+ padding: 20px;
654
+ text-align: left;
655
+ display: flex;
656
+ justify-content: space-between;
657
+ }
658
+
659
+ .header h1 {
660
+ display: inline-block;
661
+ margin: 0;
662
+ font-size: 24px;
663
+ }
664
+
665
+ .header .links {
666
+ display: flex;
667
+ align-items: center;
668
+ }
669
+ .links a {
670
+ color: #fff;
671
+ text-decoration: none;
672
+ margin-left: 30px;
673
+ }
674
+ .login-container {
675
+ max-width: 400px;
676
+ margin: 50px auto;
677
+ padding: 20px;
678
+ border: 1px solid #ccc;
679
+ border-radius: 10px;
680
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
681
+ background-color: #f9f9f9;
682
+ flex-grow: 1;
683
+ }
684
+ .login-container h1 {
685
+ text-align: center;
686
+ margin-bottom: 20px;
687
+ }
688
+ .login-container label {
689
+ display: block;
690
+ margin-bottom: 5px;
691
+ }
692
+ .login-container input {
693
+ width: calc(100% - 22px);
694
+ padding: 10px;
695
+ margin-bottom: 10px;
696
+ border: 1px solid #ccc;
697
+ border-radius: 5px;
698
+ }
699
+ .login-container button {
700
+ width: 100%;
701
+ padding: 10px;
702
+ background-color: #007BFF;
703
+ color: white;
704
+ border: none;
705
+ border-radius: 5px;
706
+ cursor: pointer;
707
+ }
708
+ .login-container button:hover {
709
+ background-color: #0056b3;
710
+ }
711
+ .notification {
712
+ display: none;
713
+ position: fixed;
714
+ top: 20px;
715
+ right: 20px;
716
+ padding: 15px;
717
+ background-color: #007BFF;
718
+ color: white;
719
+ border-radius: 5px;
720
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
721
+ }
722
+ .notification.error {
723
+ background-color: #FF0000;
724
+ }
725
+ .notification.success {
726
+ background-color: #00FF00;
727
+ }
728
+ @media (max-width: 600px) {
729
+ .login-container {
730
+ margin: 20px;
731
+ padding: 15px;
732
+ }
733
+ }
734
+ </style>
735
+ </head>
736
+ <body>
737
+ <header>
738
+ <div class="header">
739
+ <h1>OAS-Telemetry</h1>
740
+ <div class="links">
741
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">Documentation</a>
742
+ <a target="_blank" href="https://www.npmjs.com/package/@oas-tools/oas-telemetry">NPM</a>
743
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">GitHub</a>
744
+ <script>
745
+ // Check if the user is logged in
746
+ fetch('/telemetry/check')
747
+ .then(response => response.json())
748
+ .then(data => {
749
+ if (data.valid) {
750
+ const logoutLink = document.createElement('a');
751
+ logoutLink.href = '/telemetry/logout';
752
+ logoutLink.textContent = 'Logout';
753
+ document.querySelector('.links').appendChild(logoutLink);
754
+ }
755
+ })
756
+ .catch(error => console.error('Error:', error));
757
+ </script>
758
+ </div>
759
+ </div>
760
+ </header>
761
+ <main>
762
+ <div class="login-container">
763
+ <h1>Login</h1>
764
+ <form id="loginForm">
765
+ <label for="password">Password:</label>
766
+ <input type="text" id="password" name="password" required>
767
+ <button type="submit">Login</button>
768
+ </form>
769
+ </div>
770
+ </main>
771
+ <div id="notification" class="notification"></div>
772
+ <script>
773
+ function showNotification(message, type) {
774
+ const notification = document.getElementById('notification');
775
+ notification.textContent = message;
776
+ notification.className = 'notification ' + type;
777
+ notification.style.display = 'block';
778
+ setTimeout(() => {
779
+ notification.style.display = 'none';
780
+ }, 3000);
781
+ }
782
+
783
+ document.getElementById('loginForm').addEventListener('submit', async function(event) {
784
+ event.preventDefault();
785
+ const password = document.getElementById('password').value;
786
+ console.log(password);
787
+ try {
788
+ const response = await fetch('/telemetry/login', {
789
+ method: 'POST',
790
+ headers: { 'Content-Type': 'application/json' },
791
+ body: JSON.stringify({ password })
792
+ });
793
+ if (!response.ok) {
794
+ throw new Error('Network response was not ok');
795
+ }
796
+ console.log("Response:", response);
797
+ const result = await response.json();
798
+ console.log(result);
799
+ if (result.valid) {
800
+ showNotification('Login successful', 'success');
801
+ setTimeout(() => {
802
+ window.location.href = '/telemetry';
803
+ }, 1000);
804
+ } else {
805
+ showNotification('Invalid API Key', 'error');
806
+ }
807
+ } catch (error) {
808
+ console.error('Error:', error);
809
+
810
+ showNotification('An error occurred while checking the API Key.', 'error');
811
+ }
812
+ });
813
+ </script>
814
+ </body>
815
+ </html>
816
+ `,
817
+ main: `<!DOCTYPE html>
818
+ <html lang="en">
819
+
820
+ <head>
821
+ <meta charset="UTF-8">
822
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
823
+ <title>OAS - Telemetry</title>
824
+ <style>
825
+ body {
826
+ font-family: Arial, sans-serif;
827
+ margin: 0;
828
+ padding: 0;
829
+ background-color: #f5f5f5;
830
+ }
831
+
832
+ .header {
833
+ background-color: #333;
834
+ color: #fff;
835
+ padding: 20px;
836
+ text-align: left;
837
+ display: flex;
838
+ justify-content: space-between;
839
+ }
840
+
841
+ .header h1 {
842
+ display: inline-block;
843
+ margin: 0;
844
+ font-size: 24px;
845
+ }
846
+
847
+ .header .links {
848
+ display: flex;
849
+ align-items: center;
850
+ }
851
+
852
+ .links a {
853
+ color: #fff;
854
+ text-decoration: none;
855
+ margin-left: 30px;
856
+ }
857
+
858
+ .page {
859
+ margin: 0;
860
+ padding: 0;
861
+ }
862
+
863
+ .panel-conainer {
864
+ min-width: 60%;
865
+ width: fit-content;
866
+ margin: 20px auto;
867
+ }
868
+
869
+ .panel {
870
+ background-color: #fff;
871
+ border: 1px solid #ddd;
872
+ margin: 10px;
873
+ border-radius: 4px;
874
+ }
875
+
876
+ .panel-header {
877
+ background-color: #f1f1f1;
878
+ padding: 10px;
879
+ font-weight: bold;
880
+ cursor: pointer;
881
+ }
882
+
883
+ .panel-content {
884
+ display: none;
885
+ padding: 15px;
886
+
887
+ /* items margin */
888
+ >* {
889
+ margin: 5px 0;
890
+ }
891
+
892
+ }
893
+
894
+ .panel.open .panel-content {
895
+ display: flex;
896
+ flex-direction: column;
897
+ padding: 15px;
898
+ justify-content: center;
899
+ align-items: center;
900
+
901
+ }
902
+
903
+
904
+ button {
905
+ background-color: #007bff;
906
+ color: white;
907
+ border: none;
908
+ padding: 10px 15px;
909
+ cursor: pointer;
910
+ border-radius: 4px;
911
+ }
912
+
913
+ button:hover {
914
+ background-color: #0056b3;
915
+ }
916
+
917
+ table {
918
+ width: fit-content;
919
+ border-collapse: collapse;
920
+ margin: 100%;
921
+ }
922
+
923
+ th,
924
+ td {
925
+ border: 1px solid #dddddd;
926
+ padding: 8px;
927
+ text-align: left;
928
+ }
929
+
930
+ th {
931
+ background-color: #f2f2f2;
932
+ }
933
+
934
+
935
+
936
+ .spaced-row {
937
+ display: flex;
938
+ align-items: center;
939
+ justify-content: space-between;
940
+ width: 100%;
941
+ }
942
+
943
+ .row {
944
+ display: flex;
945
+ align-items: center;
946
+ }
947
+ </style>
948
+ <style>
949
+ .toggle-container {
950
+ display: flex;
951
+ flex: none;
952
+ width: max-content;
953
+ align-items: center;
954
+ margin: 10px 0;
955
+ }
956
+
957
+ .toggle {
958
+ display: flex;
959
+ align-items: center;
960
+ min-width: 40px;
961
+ min-height: 20px;
962
+ border-radius: 12px;
963
+ background-color: gray;
964
+ position: relative;
965
+ cursor: pointer;
966
+ transition: background-color 0.3s;
967
+ margin: 0 10px;
968
+ }
969
+
970
+ .toggle.circle {
971
+ border-radius: 50%;
972
+ }
973
+
974
+ .toggle.active {
975
+ background-color: green;
976
+ }
977
+
978
+ .circle-indicator {
979
+ width: 16px;
980
+ height: 16px;
981
+ margin: 0px 2px;
982
+ background-color: white;
983
+ border-radius: 50%;
984
+ transition: transform 0.3s;
985
+ }
986
+
987
+ .toggle.active .circle-indicator {
988
+ transform: translateX(20px);
989
+ /* Move the circle to the right when active */
990
+ }
991
+
992
+ .option-text {
993
+ transition: color 0.3s;
994
+ margin: 0 5px;
995
+ }
996
+ </style>
997
+ <script>
998
+ async function checkTelemetryStatus() {
999
+ try {
1000
+ const response = await fetch('/telemetry/check');
1001
+ const data = await response.json();
1002
+ if (!data.valid) {
1003
+ window.location.href = '/telemetry/login';
1004
+ }
1005
+ } catch (error) {
1006
+ console.error('Error checking telemetry status:', error);
1007
+ }
1008
+ }
1009
+
1010
+ setInterval(checkTelemetryStatus, 5000); // Check every 5 seconds
1011
+ </script>
1012
+ </head>
1013
+
1014
+ <body>
1015
+
1016
+ <div class="header">
1017
+ <h1>OAS-Telemetry</h1>
1018
+ <div class="links">
1019
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">Documentation</a>
1020
+ <a target="_blank" href="https://www.npmjs.com/package/@oas-tools/oas-telemetry">NPM</a>
1021
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">GitHub</a>
1022
+ <script>
1023
+ // Check if the user is logged in
1024
+ fetch('/telemetry/check')
1025
+ .then(response => response.json())
1026
+ .then(data => {
1027
+ if (data.valid) {
1028
+ const logoutLink = document.createElement('a');
1029
+ logoutLink.href = '/telemetry/logout';
1030
+ logoutLink.textContent = 'Logout';
1031
+ document.querySelector('.links').appendChild(logoutLink);
1032
+ }
1033
+ })
1034
+ .catch(error => console.error('Error:', error));
1035
+ </script>
1036
+ </div>
1037
+ </div>
1038
+ <div class="page">
1039
+ <div class="panel-conainer">
1040
+ <!-- Panel 1 -->
1041
+ <div class="panel open" id="panel1">
1042
+ <div class="panel-header" onclick="togglePanel('panel1')">Telemetry Management</div>
1043
+ <div class="panel-content">
1044
+ <div class="spaced-row">
1045
+ <div id="toggleTelemetry"></div>
1046
+ <button onclick="fetch('/telemetry/reset');fetchTelemetryStatus();">Reset Telemetry
1047
+ Data</button>
1048
+ </div>
1049
+ </div>
1050
+ </div>
1051
+
1052
+ <!-- Panel 2 -->
1053
+ <div class="panel open" id="panel2">
1054
+ <div class="panel-header" onclick="togglePanel('panel2')">Heap Stats</div>
1055
+ <div class="panel-content">
1056
+ <div class="spaced-row">
1057
+ <div id="heapAutoUpdate"></div>
1058
+ <button onclick="populateHeapStats()">Update</button>
1059
+ </div>
1060
+ <table id="heapStatsTable">
1061
+ <thead>
1062
+ <tr>
1063
+ <th>Stat Name</th>
1064
+ <th>Value</th>
1065
+ </tr>
1066
+ </thead>
1067
+ <tbody>
1068
+ </tbody>
1069
+ </table>
1070
+
1071
+ </div>
1072
+ </div>
1073
+
1074
+ <!-- Panel 3 -->
1075
+ <div class="panel no-user-select open" id="panel3">
1076
+ <div class="panel-header" onclick="togglePanel('panel3')">Telemetry Endpoints</div>
1077
+ <div class="panel-content">
1078
+ <div class="spaced-row">
1079
+ <div id="autoUpdateApiTable"></div>
1080
+ </div>
1081
+
1082
+ <table id="apiTable">
1083
+ <thead>
1084
+ <tr>
1085
+ <th onclick="sortTable(0)">Path</th>
1086
+ <th onclick="sortTable(1)">Method</th>
1087
+ <th onclick="sortTable(2)">Status</th>
1088
+ <th onclick="sortTable(3)">Description</th>
1089
+ <th onclick="sortTable(4)" style="text-align: center;">Request Count</th>
1090
+ <th onclick="sortTable(5)" style="text-align: center;">Average response time (sec)
1091
+ </th>
1092
+ <th style="text-align: center;">Options</th>
1093
+ </tr>
1094
+ </thead>
1095
+ <tbody>
1096
+ </tbody>
1097
+ </table>
1098
+ </table>
1099
+ </div>
1100
+ </div>
1101
+ </div>
1102
+ </div>
1103
+ <!-- Scripts -->
1104
+ <script>
1105
+ // Open Close Panel
1106
+ function togglePanel(panelId) {
1107
+ const panel = document.getElementById(panelId);
1108
+ panel.classList.toggle('open');
1109
+ }
1110
+ /**
1111
+ * Create a toggle component
1112
+ * @param {string} title - Title of the toggle
1113
+ * @param {string} falseText - Text to display when false
1114
+ * @param {string} falseColor - Color of the text when false
1115
+ * @param {string} trueText - Text to display when true
1116
+ * @param {string} trueColor - Color of the text when true
1117
+ * @param {function} handler - Function to call when the toggle is clicked
1118
+ * @param {boolean} defaultValue - Default value of the toggle
1119
+ * @returns {HTMLDivElement} - The toggle component
1120
+ */
1121
+ function createToggle(title, falseText, falseColor, trueText, trueColor, handler, defaultValue = false) {
1122
+ const container = document.createElement('div');
1123
+ container.className = 'toggle-container';
1124
+
1125
+ const label = document.createElement('span');
1126
+ label.textContent = title + ':';
1127
+ label.style.marginRight = '10px';
1128
+
1129
+ const falseTextSpan = document.createElement('span');
1130
+ falseTextSpan.className = 'option-text';
1131
+ falseTextSpan.textContent = falseText;
1132
+ falseTextSpan.style.color = defaultValue ? 'gray' : falseColor;
1133
+
1134
+ const toggle = document.createElement('div');
1135
+ toggle.className = 'toggle';
1136
+ const circleIndicator = document.createElement('div');
1137
+ circleIndicator.className = 'circle-indicator';
1138
+ toggle.appendChild(circleIndicator);
1139
+
1140
+ toggle.addEventListener('click', () => {
1141
+ toggle.classList.toggle('active');
1142
+ const isActive = toggle.classList.contains('active');
1143
+
1144
+ // Update colors. Not selected option to default color, selected option to active color
1145
+ falseTextSpan.style.color = isActive ? 'gray' : falseColor;
1146
+ trueTextSpan.style.color = isActive ? trueColor : 'gray';
1147
+
1148
+ handler(isActive);
1149
+ });
1150
+ toggle.classList.toggle('active', defaultValue);
1151
+
1152
+ const trueTextSpan = document.createElement('span');
1153
+ trueTextSpan.className = 'option-text';
1154
+ trueTextSpan.textContent = trueText; // Always display trueText
1155
+ trueTextSpan.style.color = defaultValue ? trueColor : 'gray';
1156
+
1157
+ container.appendChild(label);
1158
+ container.appendChild(falseTextSpan);
1159
+ container.appendChild(toggle);
1160
+ container.appendChild(trueTextSpan);
1161
+
1162
+ return container;
1163
+ }
1164
+
1165
+ const localStorageManager = {
1166
+ get: (key) => {
1167
+ const value = localStorage.getItem(key);
1168
+ return value ? JSON.parse(value) : null;
1169
+ },
1170
+ set: (key, value) => {
1171
+ localStorage.setItem(key, JSON.stringify(value));
1172
+ }
1173
+ };
1174
+
1175
+ </script>
1176
+ <script>
1177
+ let LOG = true;
1178
+
1179
+
1180
+ let intervalTimer = {
1181
+ disabled: true,
1182
+ period: 2000,
1183
+ subscribers: [],
1184
+ start: function () {
1185
+ this.disabled = false;
1186
+ this.interval = setInterval(() => this.tick(), this.period);
1187
+ log("interval started with period: " + this.period);
1188
+ },
1189
+ stop: function () {
1190
+ this.disabled = true;
1191
+ clearInterval(this.interval);
1192
+ log("interval stopped");
1193
+ },
1194
+ tick: function () {
1195
+ if (this.disabled) return;
1196
+ log("tick");
1197
+ this.subscribers.forEach(callback => callback());//execute all the callbacks
1198
+ },
1199
+ subscribe: function (callback) {
1200
+ this.subscribers.push(callback);
1201
+ },
1202
+ unsubscribe: function (callback) {
1203
+ this.subscribers.pop(callback)
1204
+ }
1205
+ }
1206
+
1207
+ function log(s) {
1208
+ if (LOG) console.log(s);
1209
+ }
1210
+
1211
+ async function fetchSpec() {
1212
+ try {
1213
+ const response = await fetch("/telemetry/spec");
1214
+ if (!response.ok) {
1215
+ throw new Error("ERROR getting the Spec");
1216
+ }
1217
+ apiSpec = await response.json();
1218
+ return apiSpec;
1219
+
1220
+ } catch (error) {
1221
+ console.error("ERROR getting the Spec :", error);
1222
+ return null;
1223
+ }
1224
+ }
1225
+
1226
+ async function fetchTelemetryStatus() {
1227
+ const response = await fetch("/telemetry/status");
1228
+ if (!response.ok) {
1229
+ throw new Error("ERROR getting the Status");
1230
+ return false;
1231
+ }
1232
+ tStatus = await response.json();
1233
+
1234
+ log("tStatus: " + JSON.stringify(tStatus, null, 2));
1235
+ return tStatus.active;
1236
+ }
1237
+
1238
+ function getPathRegEx(path) {
1239
+ let pathComponents = path.split("/");
1240
+ let pathRegExpStr = "^"
1241
+
1242
+ pathComponents.forEach(c => {
1243
+ if (c != "") {
1244
+ pathRegExpStr += "/";
1245
+ if (c.charAt(0) == "{" && c.charAt(c.length - 1) == "}") {
1246
+ // Ensure it matches at least one character (.+)
1247
+ pathRegExpStr += "(.+)";
1248
+ } else {
1249
+ pathRegExpStr += c;
1250
+ }
1251
+ }
1252
+ });
1253
+
1254
+ // Allow an optional trailing slash
1255
+ pathRegExpStr += "/?\$";
1256
+
1257
+ return pathRegExpStr;
1258
+ }
1259
+
1260
+ async function fetchTracesByFind(path, method, status) {
1261
+ try {
1262
+ let statusOr = [{ "attributes.http.status_code": parseInt(status) }]
1263
+ if (status == "200") {
1264
+ statusOr.push({ "attributes.http.status_code": 304 }); //Some servers return 304 instead of 200
1265
+ }
1266
+ log(\`Fetching traces for <\${path}> - \${method} - \${status} \`);
1267
+ const body = {
1268
+ "flags": { "containsRegex": true },
1269
+ "config": { "regexIds": ["attributes.http.target", "attributes.http.status_code"] },
1270
+ "search": {
1271
+ "attributes.http.target": getPathRegEx(path),
1272
+ "attributes.http.method": method.toUpperCase(),
1273
+ "\$or": statusOr
1274
+ }
1275
+ };
1276
+ log("body: " + JSON.stringify(body, null, 2));
1277
+ //response is to the post at /telemetry/find
1278
+ const response = await fetch("/telemetry/find", {
1279
+ method: "POST",
1280
+ headers: {
1281
+ "Content-Type": "application/json"
1282
+ },
1283
+ body: JSON.stringify(body)
1284
+ });
1285
+
1286
+ if (!response.ok) {
1287
+ throw new Error("ERROR getting the Traces.");
1288
+ }
1289
+
1290
+ const responseJSON = await response.json();
1291
+ const traces = responseJSON.spans;
1292
+
1293
+ log(\`Fetched \${traces.length} traces.\`);
1294
+ return traces;
1295
+
1296
+ } catch (error) {
1297
+ console.error("ERROR getting the Traces :", error);
1298
+ }
1299
+ }
1300
+
1301
+ function calculateTiming(startSecInput, startNanoSecInput, endSecInput, endNanoSecInput, precision = 3) {
1302
+ // Convert inputs to numbers
1303
+ let startSec = parseInt(startSecInput);
1304
+ let startNanoSec = parseInt(startNanoSecInput);
1305
+ let endSec = parseInt(endSecInput);
1306
+ let endNanoSec = parseInt(endNanoSecInput);
1307
+
1308
+ // Convert nanoseconds to fractional seconds and add to seconds
1309
+ let preciseStart = startSec + startNanoSec / 1e9; // Nanoseconds to seconds
1310
+ let preciseEnd = endSec + endNanoSec / 1e9; // Nanoseconds to seconds
1311
+
1312
+ // Calculate duration
1313
+ let preciseDuration = preciseEnd - preciseStart;
1314
+
1315
+ // Create Date objects and ISO timestamps
1316
+ let startDate = new Date(preciseStart * 1000);
1317
+ let endDate = new Date(preciseEnd * 1000);
1318
+
1319
+ return {
1320
+ preciseStart: preciseStart, // Precise start time in seconds
1321
+ preciseEnd: preciseEnd, // Precise end time in seconds
1322
+ preciseDuration: preciseDuration, // Duration in seconds
1323
+ start: parseFloat(preciseStart.toFixed(precision)), // Rounded start time
1324
+ end: parseFloat(preciseEnd.toFixed(precision)), // Rounded end time
1325
+ duration: parseFloat(preciseDuration.toFixed(precision)), // Rounded duration
1326
+ startDate: startDate, // Date object for start time
1327
+ endDate: endDate, // Date object for end time
1328
+ startTS: startDate.toISOString(), // ISO timestamp for start
1329
+ endTS: endDate.toISOString() // ISO timestamp for end
1330
+ };
1331
+ }
1332
+
1333
+
1334
+ function parseTraceInfo(t) {
1335
+ const ep = t.attributes.http.target;
1336
+ const method = t.attributes.http.method.toLowerCase();
1337
+ const status = t.attributes.http.status_code;
1338
+
1339
+ const timing = calculateTiming(t.startTime[0], t.startTime[1], t.endTime[0], t.endTime[1]);
1340
+
1341
+ log(\`\${timing.startTS} - \${timing.endTS} - \${t._spanContext.traceId} - \${t.name} - \${ep} - \${status} - \${timing.duration}\`);
1342
+ return {
1343
+ ts: timing.startTS,
1344
+ ep: ep,
1345
+ method: method,
1346
+ status: status,
1347
+ duration: timing.duration
1348
+ };
1349
+ }
1350
+
1351
+ async function loadStats(path, method, status, cellRequestCount, cellAverageResponseTime) {
1352
+ log(\`loadStats(\${path}, \${method}, \${status}, \${cellRequestCount}, \${cellAverageResponseTime})\`);
1353
+ let traces = await fetchTracesByFind(path, method, status);
1354
+ let requestCount = traces.length;
1355
+ let averageResponseTime = 0;
1356
+
1357
+ traces.forEach(trace => {
1358
+ t = parseTraceInfo(trace);
1359
+ log(JSON.stringify(t, null, 2));
1360
+ averageResponseTime += parseFloat(t.duration);
1361
+ log(\`averageResponseTime += t.duration --> \${averageResponseTime} += \${t.duration}\`);
1362
+ });
1363
+
1364
+ averageResponseTime = averageResponseTime / requestCount;
1365
+
1366
+ log(\`averageResponseTime = averageResponseTime / requestCount --> \${averageResponseTime} = \${averageResponseTime} / \${requestCount}\`);
1367
+
1368
+ cellRequestCount.textContent = requestCount;
1369
+ cellAverageResponseTime.textContent = requestCount ? averageResponseTime.toFixed(3) : "--";
1370
+
1371
+ }
1372
+
1373
+ async function populateApiTable() {
1374
+ const apiSpec = await fetchSpec()
1375
+ const tableBody = document.getElementById('apiTable').getElementsByTagName('tbody')[0];
1376
+ tableBody.innerHTML = "";
1377
+ Object.keys(apiSpec.paths).forEach(path => {
1378
+ Object.keys(apiSpec.paths[path]).forEach(method => {
1379
+ Object.keys(apiSpec.paths[path][method].responses).forEach(responseType => {
1380
+ if (!Number.isNaN(parseInt(responseType))) {
1381
+ const row = tableBody.insertRow();
1382
+ const cellPath = row.insertCell(0);
1383
+ const cellMethod = row.insertCell(1);
1384
+ const cellStatus = row.insertCell(2);
1385
+ const cellDescription = row.insertCell(3);
1386
+ const cellRequestCount = row.insertCell(4);
1387
+ cellRequestCount.style = "text-align: center;";
1388
+ cellRequestCount.textContent = "--";
1389
+ const cellAverageResponseTime = row.insertCell(5);
1390
+ cellAverageResponseTime.style.textAlign = "center";
1391
+ cellAverageResponseTime.textContent = "--";
1392
+
1393
+ const basePath = apiSpec.basePath ? apiSpec.basePath : "";
1394
+ const fullPath = basePath + path;
1395
+ cellPath.textContent = path;
1396
+ cellPath.style.cursor = 'pointer';
1397
+ cellPath.onclick = function () {
1398
+ window.location.href = row.detailPath;
1399
+ };
1400
+ cellMethod.textContent = method.toUpperCase();
1401
+ cellStatus.textContent = responseType;
1402
+ cellDescription.textContent = apiSpec.paths[path][method].summary
1403
+ + " - "
1404
+ + apiSpec.paths[path][method].responses[responseType].description;
1405
+
1406
+ row.detailPath = \`/telemetry/detail/\${responseType}/\${method.toLowerCase()}\${fullPath}\`;
1407
+ const cellOptions = row.insertCell(6);
1408
+
1409
+
1410
+ // Create a button for updating the endpoint spaced row but centered
1411
+ const updateButton = document.createElement('button');
1412
+ updateButton.textContent = "Update";
1413
+ updateButton.onclick = () => loadStats(path, method, responseType, cellRequestCount, cellAverageResponseTime);
1414
+ cellOptions.appendChild(updateButton);
1415
+ cellOptions.style.display = "flex";
1416
+ cellOptions.style.justifyContent = "center";
1417
+ loadStats(path, method, responseType, cellRequestCount, cellAverageResponseTime);
1418
+
1419
+ }
1420
+ });
1421
+ });
1422
+ });
1423
+ }
1424
+
1425
+ function populateHeapStats() {
1426
+ // heapstats at /telemetry/heapstats
1427
+ const tableBody = document.getElementById('heapStatsTable').getElementsByTagName('tbody')[0];
1428
+ fetch('/telemetry/heapstats').then(response => response.json()).then(heapStats => {
1429
+ tableBody.innerHTML = "";
1430
+ Object.keys(heapStats).forEach(statName => {
1431
+ const row = tableBody.insertRow();
1432
+ const cellStatName = row.insertCell(0);
1433
+ const cellValue = row.insertCell(1);
1434
+ cellStatName.textContent = statName;
1435
+ //format always to 3 decimals (if number)
1436
+ const formattedValue = typeof heapStats[statName] === 'number' ? heapStats[statName].toFixed(3) : heapStats[statName];
1437
+ cellValue.textContent = heapStats[statName];
1438
+ cellValue.style.textAlign = "right";
1439
+ });
1440
+ });
1441
+ }
1442
+
1443
+ function sortTable(column) {
1444
+ const table = document.getElementById('apiTable');
1445
+ let rows, switching, i, x, y, shouldSwitch;
1446
+ switching = true;
1447
+ while (switching) {
1448
+ switching = false;
1449
+ rows = table.rows;
1450
+ for (i = 1; i < (rows.length - 1); i++) {
1451
+ shouldSwitch = false;
1452
+ x = rows[i].getElementsByTagName("TD")[column];
1453
+ y = rows[i + 1].getElementsByTagName("TD")[column];
1454
+ if (x.textContent.toLowerCase() > y.textContent.toLowerCase()) {
1455
+ shouldSwitch = true;
1456
+ break;
1457
+ }
1458
+ }
1459
+ if (shouldSwitch) {
1460
+ rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
1461
+ switching = true;
1462
+ }
1463
+ }
1464
+ }
1465
+
1466
+ document.getElementById('toggleTelemetry').appendChild(createToggle(
1467
+ 'Telemetry status',
1468
+ 'Stopped', 'red', // false state
1469
+ 'Active', 'green', // true state
1470
+ async (status) => {
1471
+ const response = await fetch('/telemetry/' + (status ? 'start' : 'stop'));
1472
+ if (!response.ok) {
1473
+ throw new Error("ERROR setting the Telemetry status");
1474
+ }
1475
+ }
1476
+ ));
1477
+
1478
+ document.getElementById('autoUpdateApiTable').appendChild(createToggle(
1479
+ 'Auto Update',
1480
+ 'Manual', 'orange', // false state
1481
+ 'Auto', 'green', // true state
1482
+ (status) => {
1483
+ const callback = () => { populateApiTable(); };
1484
+ if (status) {
1485
+ intervalTimer.subscribe(callback);
1486
+ } else {
1487
+ intervalTimer.unsubscribe(callback);
1488
+ }
1489
+ }
1490
+ ));
1491
+
1492
+ document.getElementById('heapAutoUpdate').appendChild(createToggle(
1493
+ 'Auto Update',
1494
+ 'Manual', 'orange', // false state
1495
+ 'Auto', 'green', // true state
1496
+ (status) => {
1497
+ if (status) {
1498
+ intervalTimer.subscribe(populateHeapStats);
1499
+ } else {
1500
+ intervalTimer.unsubscribe(populateHeapStats);
1501
+ }
1502
+ }
1503
+ ));
1504
+
1505
+ window.onload = async function () {
1506
+ document.getElementById('autoUpdateApiTable').querySelector('.toggle').classList.toggle('active', localStorageManager.get('autoUpdateApiTable'));
1507
+ const activeTelemetry = await fetchTelemetryStatus();
1508
+ document.getElementById('toggleTelemetry').querySelector('.toggle').classList.toggle('active', activeTelemetry);
1509
+ populateApiTable();
1510
+ populateHeapStats();
1511
+ intervalTimer.start();
1512
+ };
1513
+ </script>
1514
+ </body>
1515
+
1516
+ </html>`,
1517
+ };
1518
+
1519
+
1520
+ export default ui