@oas-tools/oas-telemetry 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui.cjs CHANGED
@@ -6,8 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = ui;
7
7
  function ui() {
8
8
  return {
9
- main: `
10
- <!DOCTYPE html>
9
+ main: `<!DOCTYPE html>
11
10
  <html lang="en">
12
11
 
13
12
  <head>
@@ -17,12 +16,98 @@ function ui() {
17
16
  <style>
18
17
  body {
19
18
  font-family: Arial, sans-serif;
20
- margin: 20px;
19
+ margin: 0;
20
+ padding: 0;
21
+ background-color: #f5f5f5;
22
+ }
23
+
24
+ .header {
25
+ background-color: #333;
26
+ color: #fff;
27
+ padding: 20px;
28
+ text-align: left;
29
+ display: flex;
30
+ justify-content: space-between;
31
+ }
32
+
33
+ .header h1 {
34
+ display: inline-block;
35
+ margin: 0;
36
+ font-size: 24px;
37
+ }
38
+
39
+ .header .links {
40
+ display: flex;
41
+ align-items: center;
42
+ }
43
+ .links a {
44
+ color: #fff;
45
+ text-decoration: none;
46
+ margin-left: 30px;
47
+ }
48
+
49
+ .page {
50
+ margin: 0;
51
+ padding: 0;
52
+ }
53
+
54
+ .panel-conainer {
55
+ min-width: 60%;
56
+ width: fit-content;
57
+ margin: 20px auto;
58
+ }
59
+ .panel {
60
+ background-color: #fff;
61
+ border: 1px solid #ddd;
62
+ margin: 10px;
63
+ border-radius: 4px;
64
+ }
65
+
66
+ .panel-header {
67
+ background-color: #f1f1f1;
68
+ padding: 10px;
69
+ font-weight: bold;
70
+ cursor: pointer;
71
+ }
72
+
73
+ .panel-content {
74
+ display: none;
75
+ padding: 15px;
76
+
77
+ /* items margin */
78
+ >* {
79
+ margin: 5px 0;
80
+ }
81
+
82
+ }
83
+
84
+ .panel.open .panel-content {
85
+ display: flex;
86
+ flex-direction: column;
87
+ padding: 15px;
88
+ justify-content: center;
89
+ align-items: center;
90
+
91
+ }
92
+
93
+
94
+ button {
95
+ background-color: #007bff;
96
+ color: white;
97
+ border: none;
98
+ padding: 10px 15px;
99
+ cursor: pointer;
100
+ border-radius: 4px;
101
+ }
102
+
103
+ button:hover {
104
+ background-color: #0056b3;
21
105
  }
22
106
 
23
107
  table {
24
- width: 100%;
108
+ width: fit-content;
25
109
  border-collapse: collapse;
110
+ margin: 100%;
26
111
  }
27
112
 
28
113
  th,
@@ -36,40 +121,221 @@ function ui() {
36
121
  background-color: #f2f2f2;
37
122
  }
38
123
 
39
- #telemetryStatusSpan {
40
- color: rgb(0, 123, 6);
41
- font-size: x-small;
124
+
125
+
126
+ .spaced-row {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: space-between;
130
+ width: 100%;
131
+ }
132
+
133
+ .row {
134
+ display: flex;
135
+ align-items: center;
136
+ }
137
+ </style>
138
+ <style>
139
+ .toggle-container {
140
+ display: flex;
141
+ flex: none;
142
+ width: max-content;
143
+ align-items: center;
144
+ margin: 10px 0;
145
+ }
146
+
147
+ .toggle {
148
+ display: flex;
149
+ align-items: center;
150
+ min-width: 40px;
151
+ min-height: 20px;
152
+ border-radius: 12px;
153
+ background-color: gray;
154
+ position: relative;
155
+ cursor: pointer;
156
+ transition: background-color 0.3s;
157
+ margin: 0 10px;
158
+ }
159
+
160
+ .toggle.circle {
161
+ border-radius: 50%;
162
+ }
163
+
164
+ .toggle.active {
165
+ background-color: green;
166
+ }
167
+
168
+ .circle-indicator {
169
+ width: 16px;
170
+ height: 16px;
171
+ margin: 0px 2px;
172
+ background-color: white;
173
+ border-radius: 50%;
174
+ transition: transform 0.3s;
175
+ }
176
+
177
+ .toggle.active .circle-indicator {
178
+ transform: translateX(20px);
179
+ /* Move the circle to the right when active */
180
+ }
181
+
182
+ .option-text {
183
+ transition: color 0.3s;
184
+ margin: 0 5px;
42
185
  }
43
186
  </style>
44
187
  </head>
45
188
 
46
189
  <body>
47
- <h1>Telemetry <span id="telemetryStatusSpan"></span></h1>
48
-
49
- <label><input type="checkbox" id="toggleTelemetry"> Allow Server Telemetry</label>
50
- <br><br>
51
- <button onclick="fetch('/telemetry/reset');showTelemetryStatus();">Reset Telemetry Data</button>
52
- <br><br>
53
- <label><input type="checkbox" id="autoUpdate"> Allow Client Auto Update</label>
54
- <br><br>
55
- <table id="apiTable">
56
- <thead>
57
- <tr>
58
- <th onclick="sortTable(0)">Path</th>
59
- <th onclick="sortTable(1)">Method</th>
60
- <th onclick="sortTable(2)">Status</th>
61
- <th onclick="sortTable(3)">Description</th>
62
- <th onclick="sortTable(4)" style="text-align: center;">Request <br> Count</th>
63
- <th onclick="sortTable(5)" style="text-align: center;">Average response time<br> (sec)</th>
64
- <th style="text-align: center;">Auto<br>Update</th>
65
- </tr>
66
- </thead>
67
- <tbody>
68
- </tbody>
69
- </table>
70
190
 
191
+ <div class="header">
192
+ <h1>OAS-Telemetry</h1>
193
+ <div class="links">
194
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">Documentation</a>
195
+ <a target="_blank" href="https://www.npmjs.com/package/@oas-tools/oas-telemetry">NPM</a>
196
+ <a target="_blank" href="https://github.com/oas-tools/oas-telemetry">GitHub</a>
197
+ </div>
198
+ </div>
199
+ <div class="page">
200
+ <div class="panel-conainer">
201
+ <!-- Panel 1 -->
202
+ <div class="panel open" id="panel1">
203
+ <div class="panel-header" onclick="togglePanel('panel1')">Telemetry Management</div>
204
+ <div class="panel-content">
205
+ <div class="spaced-row">
206
+ <div id="toggleTelemetry"></div>
207
+ <button onclick="fetch('/telemetry/reset');fetchTelemetryStatus();">Reset Telemetry
208
+ Data</button>
209
+ </div>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Panel 2 -->
214
+ <div class="panel open" id="panel2">
215
+ <div class="panel-header" onclick="togglePanel('panel2')">Heap Stats</div>
216
+ <div class="panel-content">
217
+ <div class="spaced-row">
218
+ <div id="heapAutoUpdate"></div>
219
+ <button onclick="populateHeapStats()">Update</button>
220
+ </div>
221
+ <table id="heapStatsTable">
222
+ <thead>
223
+ <tr>
224
+ <th>Stat Name</th>
225
+ <th>Value</th>
226
+ </tr>
227
+ </thead>
228
+ <tbody>
229
+ </tbody>
230
+ </table>
231
+
232
+ </div>
233
+ </div>
234
+
235
+ <!-- Panel 3 -->
236
+ <div class="panel no-user-select open" id="panel3">
237
+ <div class="panel-header" onclick="togglePanel('panel3')">Telemetry Endpoints</div>
238
+ <div class="panel-content">
239
+ <div class="spaced-row">
240
+ <div id="autoUpdateApiTable"></div>
241
+ </div>
242
+
243
+ <table id="apiTable">
244
+ <thead>
245
+ <tr>
246
+ <th onclick="sortTable(0)">Path</th>
247
+ <th onclick="sortTable(1)">Method</th>
248
+ <th onclick="sortTable(2)">Status</th>
249
+ <th onclick="sortTable(3)">Description</th>
250
+ <th onclick="sortTable(4)" style="text-align: center;">Request Count</th>
251
+ <th onclick="sortTable(5)" style="text-align: center;">Average response time (sec)
252
+ </th>
253
+ <th style="text-align: center;">Options</th>
254
+ </tr>
255
+ </thead>
256
+ <tbody>
257
+ </tbody>
258
+ </table>
259
+ </table>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ <!-- Scripts -->
71
265
  <script>
72
- let LOG = false;
266
+ // Open Close Panel
267
+ function togglePanel(panelId) {
268
+ const panel = document.getElementById(panelId);
269
+ panel.classList.toggle('open');
270
+ }
271
+ /**
272
+ * Create a toggle component
273
+ * @param {string} title - Title of the toggle
274
+ * @param {string} falseText - Text to display when false
275
+ * @param {string} falseColor - Color of the text when false
276
+ * @param {string} trueText - Text to display when true
277
+ * @param {string} trueColor - Color of the text when true
278
+ * @param {function} handler - Function to call when the toggle is clicked
279
+ * @param {boolean} defaultValue - Default value of the toggle
280
+ * @returns {HTMLDivElement} - The toggle component
281
+ */
282
+ function createToggle(title, falseText, falseColor, trueText, trueColor, handler, defaultValue = false) {
283
+ const container = document.createElement('div');
284
+ container.className = 'toggle-container';
285
+
286
+ const label = document.createElement('span');
287
+ label.textContent = title + ':';
288
+ label.style.marginRight = '10px';
289
+
290
+ const falseTextSpan = document.createElement('span');
291
+ falseTextSpan.className = 'option-text';
292
+ falseTextSpan.textContent = falseText;
293
+ falseTextSpan.style.color = defaultValue ? 'gray' : falseColor;
294
+
295
+ const toggle = document.createElement('div');
296
+ toggle.className = 'toggle';
297
+ const circleIndicator = document.createElement('div');
298
+ circleIndicator.className = 'circle-indicator';
299
+ toggle.appendChild(circleIndicator);
300
+
301
+ toggle.addEventListener('click', () => {
302
+ toggle.classList.toggle('active');
303
+ const isActive = toggle.classList.contains('active');
304
+
305
+ // Update colors. Not selected option to default color, selected option to active color
306
+ falseTextSpan.style.color = isActive ? 'gray' : falseColor;
307
+ trueTextSpan.style.color = isActive ? trueColor : 'gray';
308
+
309
+ handler(isActive);
310
+ });
311
+ toggle.classList.toggle('active', defaultValue);
312
+
313
+ const trueTextSpan = document.createElement('span');
314
+ trueTextSpan.className = 'option-text';
315
+ trueTextSpan.textContent = trueText; // Always display trueText
316
+ trueTextSpan.style.color = defaultValue ? trueColor : 'gray';
317
+
318
+ container.appendChild(label);
319
+ container.appendChild(falseTextSpan);
320
+ container.appendChild(toggle);
321
+ container.appendChild(trueTextSpan);
322
+
323
+ return container;
324
+ }
325
+
326
+ const localStorageManager = {
327
+ get: (key) => {
328
+ const value = localStorage.getItem(key);
329
+ return value ? JSON.parse(value) : null;
330
+ },
331
+ set: (key, value) => {
332
+ localStorage.setItem(key, JSON.stringify(value));
333
+ }
334
+ };
335
+
336
+ </script>
337
+ <script>
338
+ let LOG = true;
73
339
 
74
340
 
75
341
  let intervalTimer = {
@@ -77,7 +343,14 @@ function ui() {
77
343
  period: 2000,
78
344
  subscribers: [],
79
345
  start: function () {
346
+ this.disabled = false;
80
347
  this.interval = setInterval(() => this.tick(), this.period);
348
+ log("interval started with period: " + this.period);
349
+ },
350
+ stop: function () {
351
+ this.disabled = true;
352
+ clearInterval(this.interval);
353
+ log("interval stopped");
81
354
  },
82
355
  tick: function () {
83
356
  if (this.disabled) return;
@@ -92,10 +365,6 @@ function ui() {
92
365
  }
93
366
  }
94
367
 
95
-
96
-
97
-
98
-
99
368
  function log(s) {
100
369
  if (LOG) console.log(s);
101
370
  }
@@ -115,6 +384,17 @@ function ui() {
115
384
  }
116
385
  }
117
386
 
387
+ async function fetchTelemetryStatus() {
388
+ const response = await fetch("/telemetry/status");
389
+ if (!response.ok) {
390
+ throw new Error("ERROR getting the Status");
391
+ return false;
392
+ }
393
+ tStatus = await response.json();
394
+
395
+ log("tStatus: " + JSON.stringify(tStatus, null, 2));
396
+ return tStatus.active;
397
+ }
118
398
 
119
399
  function getPathRegEx(path) {
120
400
  let pathComponents = path.split("/");
@@ -124,14 +404,16 @@ function ui() {
124
404
  if (c != "") {
125
405
  pathRegExpStr += "/";
126
406
  if (c.charAt(0) == "{" && c.charAt(c.length - 1) == "}") {
127
- pathRegExpStr += "(.*)";
407
+ // Ensure it matches at least one character (.+)
408
+ pathRegExpStr += "(.+)";
128
409
  } else {
129
410
  pathRegExpStr += c;
130
411
  }
131
412
  }
132
413
  });
133
414
 
134
- pathRegExpStr += "\$";
415
+ // Allow an optional trailing slash
416
+ pathRegExpStr += "/?\$";
135
417
 
136
418
  return pathRegExpStr;
137
419
  }
@@ -243,11 +525,12 @@ function ui() {
243
525
  cellRequestCount.textContent = requestCount;
244
526
  cellAverageResponseTime.textContent = requestCount ? averageResponseTime.toFixed(3) : "--";
245
527
 
246
- // setTimeout(() => loadStats(path, method, status, cellRequestCount, cellAverageResponseTime), 2000);
247
528
  }
248
529
 
249
- function populateOASTable(apiSpec) {
530
+ async function populateApiTable() {
531
+ const apiSpec = await fetchSpec()
250
532
  const tableBody = document.getElementById('apiTable').getElementsByTagName('tbody')[0];
533
+ tableBody.innerHTML = "";
251
534
  Object.keys(apiSpec.paths).forEach(path => {
252
535
  Object.keys(apiSpec.paths[path]).forEach(method => {
253
536
  Object.keys(apiSpec.paths[path][method].responses).forEach(responseType => {
@@ -278,28 +561,17 @@ function ui() {
278
561
  + apiSpec.paths[path][method].responses[responseType].description;
279
562
 
280
563
  row.detailPath = \`/telemetry/detail/\${responseType}/\${method.toLowerCase()}\${fullPath}\`;
281
- const cellCheckbox = row.insertCell(6);
282
-
283
- // Create and insert checkbox
284
- const checkbox = document.createElement('input');
285
- checkbox.type = 'checkbox';
286
- cellCheckbox.appendChild(checkbox);
287
- cellCheckbox.style.textAlign = 'center';
288
- // class to search for the checkboxes
289
- checkbox.className = 'autoUpdateCheckbox';
290
- checkbox.disabled = intervalTimer.disabled;
291
-
292
- // Add event listener to handle checkbox auto-update
293
- checkbox.addEventListener('change', function () {
294
- if (this.checked)
295
- intervalTimer.subscribe(() =>
296
- loadStats(fullPath, method.toLowerCase(), responseType, cellRequestCount, cellAverageResponseTime)
297
- );
298
- else
299
- intervalTimer.unsubscribe(() =>
300
- loadStats(fullPath, method.toLowerCase(), responseType, cellRequestCount, cellAverageResponseTime)
301
- );
302
- });
564
+ const cellOptions = row.insertCell(6);
565
+
566
+
567
+ // Create a button for updating the endpoint spaced row but centered
568
+ const updateButton = document.createElement('button');
569
+ updateButton.textContent = "Update";
570
+ updateButton.onclick = () => loadStats(path, method, responseType, cellRequestCount, cellAverageResponseTime);
571
+ cellOptions.appendChild(updateButton);
572
+ cellOptions.style.display = "flex";
573
+ cellOptions.style.justifyContent = "center";
574
+ loadStats(path, method, responseType, cellRequestCount, cellAverageResponseTime);
303
575
 
304
576
  }
305
577
  });
@@ -307,6 +579,24 @@ function ui() {
307
579
  });
308
580
  }
309
581
 
582
+ function populateHeapStats() {
583
+ // heapstats at /telemetry/heapstats
584
+ const tableBody = document.getElementById('heapStatsTable').getElementsByTagName('tbody')[0];
585
+ fetch('/telemetry/heapstats').then(response => response.json()).then(heapStats => {
586
+ tableBody.innerHTML = "";
587
+ Object.keys(heapStats).forEach(statName => {
588
+ const row = tableBody.insertRow();
589
+ const cellStatName = row.insertCell(0);
590
+ const cellValue = row.insertCell(1);
591
+ cellStatName.textContent = statName;
592
+ //format always to 3 decimals (if number)
593
+ const formattedValue = typeof heapStats[statName] === 'number' ? heapStats[statName].toFixed(3) : heapStats[statName];
594
+ cellValue.textContent = heapStats[statName];
595
+ cellValue.style.textAlign = "right";
596
+ });
597
+ });
598
+ }
599
+
310
600
  function sortTable(column) {
311
601
  const table = document.getElementById('apiTable');
312
602
  let rows, switching, i, x, y, shouldSwitch;
@@ -330,58 +620,58 @@ function ui() {
330
620
  }
331
621
  }
332
622
 
333
- document.getElementById('autoUpdate').addEventListener('change', function () {
334
- const autoUpdateDisabled = !this.checked;
335
- intervalTimer.disabled = autoUpdateDisabled;
336
- // Disable/Enable all the checkboxes
337
- const checkboxes = document.getElementsByClassName('autoUpdateCheckbox');
338
- for (let i = 0; i < checkboxes.length; i++) {
339
- checkboxes[i].disabled = autoUpdateDisabled;
340
- }
341
- });
342
-
343
- async function showTelemetryStatus() {
344
- const tss = document.getElementById("telemetryStatusSpan");
345
- const response = await fetch("/telemetry/status");
346
- if (!response.ok) {
347
- throw new Error("ERROR getting the Status");
348
- return false;
623
+ document.getElementById('toggleTelemetry').appendChild(createToggle(
624
+ 'Telemetry status',
625
+ 'Stopped', 'red', // false state
626
+ 'Active', 'green', // true state
627
+ async (status) => {
628
+ const response = await fetch('/telemetry/' + (status ? 'start' : 'stop'));
629
+ if (!response.ok) {
630
+ throw new Error("ERROR setting the Telemetry status");
631
+ }
349
632
  }
350
- tStatus = await response.json();
351
-
352
- log(tStatus);
353
- if (tStatus.active) {
354
- tss.textContent = "active";
355
- tss.style.color = "#009900";
356
- } else {
357
- tss.textContent = "stopped";
358
- tss.style.color = "#666666";
633
+ ));
634
+
635
+ document.getElementById('autoUpdateApiTable').appendChild(createToggle(
636
+ 'Auto Update',
637
+ 'Manual', 'orange', // false state
638
+ 'Auto', 'green', // true state
639
+ (status) => {
640
+ const callback = () => { populateApiTable(); };
641
+ if (status) {
642
+ intervalTimer.subscribe(callback);
643
+ } else {
644
+ intervalTimer.unsubscribe(callback);
645
+ }
359
646
  }
360
- return tStatus.active;
361
- }
362
-
363
- document.getElementById('toggleTelemetry').addEventListener('change', function () {
364
- if (this.checked) {
365
- fetch('/telemetry/start').then(showTelemetryStatus());
366
- } else {
367
- fetch('/telemetry/stop').then(showTelemetryStatus());
647
+ ));
648
+
649
+ document.getElementById('heapAutoUpdate').appendChild(createToggle(
650
+ 'Auto Update',
651
+ 'Manual', 'orange', // false state
652
+ 'Auto', 'green', // true state
653
+ (status) => {
654
+ if (status) {
655
+ intervalTimer.subscribe(populateHeapStats);
656
+ } else {
657
+ intervalTimer.unsubscribe(populateHeapStats);
658
+ }
368
659
  }
369
- });
660
+ ));
370
661
 
371
662
  window.onload = async function () {
372
- document.getElementById('autoUpdate').checked = !intervalTimer.disabled; // Default to false
373
- const activeTelemetry = await showTelemetryStatus();
374
- document.getElementById('toggleTelemetry').checked = activeTelemetry; // Default to not toggled
375
- const apiSpec = await fetchSpec()
376
- populateOASTable(apiSpec);
663
+ document.getElementById('autoUpdateApiTable').querySelector('.toggle').classList.toggle('active', localStorageManager.get('autoUpdateApiTable'));
664
+ const activeTelemetry = await fetchTelemetryStatus();
665
+ document.getElementById('toggleTelemetry').querySelector('.toggle').classList.toggle('active', activeTelemetry);
666
+ populateApiTable();
667
+ populateHeapStats();
377
668
  intervalTimer.start();
378
669
  };
379
670
  </script>
380
671
  </body>
381
672
 
382
673
  </html>`,
383
- detail: `
384
- <!DOCTYPE html>
674
+ detail: `<!DOCTYPE html>
385
675
  <html lang="en">
386
676
 
387
677
  <head>
@@ -584,14 +874,16 @@ function ui() {
584
874
  if (c != "") {
585
875
  pathRegExpStr += "/";
586
876
  if (c.charAt(0) == "{" && c.charAt(c.length - 1) == "}") {
587
- pathRegExpStr += "(.*)";
877
+ // Ensure it matches at least one character (.+)
878
+ pathRegExpStr += "(.+)";
588
879
  } else {
589
880
  pathRegExpStr += c;
590
881
  }
591
882
  }
592
883
  });
593
884
 
594
- pathRegExpStr += "\$";
885
+ // Allow an optional trailing slash
886
+ pathRegExpStr += "/?\$";
595
887
 
596
888
  return pathRegExpStr;
597
889
  }
@@ -600,7 +892,24 @@ function ui() {
600
892
  try {
601
893
  log(\`Fetchig traces for <\${path}> - \${method} - \${status},.. \`);
602
894
 
603
- const response = await fetch("/telemetry/list");
895
+ const body = {
896
+ "flags": { "containsRegex": true },
897
+ "config": { "regexIds": ["attributes.http.target"] },
898
+ "search": {
899
+ "attributes.http.target": getPathRegEx(path),
900
+ "attributes.http.method": method.toUpperCase(),
901
+ "attributes.http.status_code": parseInt(status)
902
+ }
903
+ };
904
+ log("body: " + JSON.stringify(body, null, 2));
905
+ //response is to the post at /telemetry/find
906
+ const response = await fetch("/telemetry/find", {
907
+ method: "POST",
908
+ headers: {
909
+ "Content-Type": "application/json"
910
+ },
911
+ body: JSON.stringify(body)
912
+ });
604
913
 
605
914
  if (!response.ok) {
606
915
  throw new Error("ERROR getting the Traces.");
@@ -608,13 +917,12 @@ function ui() {
608
917
 
609
918
  const responseJSON = await response.json();
610
919
  const traces = responseJSON.spans;
611
- const filteredTraces = traces; //filter made in server side
612
- log(\`Feched \${traces.length} traces.\`);
613
- //log(\`First trace: \${JSON.stringify(traces[0],null,2)}\`);
614
920
 
615
- if (filteredTraces.length != traceCount) {
616
- loadTraces(filteredTraces);
617
- traceCount = filteredTraces.length;
921
+ log(\`Fetched \${traces.length} traces.\`);
922
+
923
+ if (traces.length != traceCount) {
924
+ loadTraces(traces);
925
+ traceCount = traces.length;
618
926
  }
619
927
 
620
928
  setTimeout(fetchTracesByParsing, 1000, path, method, status);