web_plsql 0.18.0 → 1.2.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 (74) hide show
  1. package/README.md +29 -9
  2. package/package.json +17 -7
  3. package/src/admin/client/charts.ts +598 -0
  4. package/src/admin/client/main.js +3 -0
  5. package/src/admin/client/tailwind.css +41 -0
  6. package/src/admin/favicon.svg +3 -0
  7. package/src/admin/index.html +529 -0
  8. package/src/admin/js/api.ts +170 -0
  9. package/src/admin/js/app.ts +614 -0
  10. package/src/admin/js/eslint.config.js +74 -0
  11. package/src/admin/js/schemas.ts +215 -0
  12. package/src/admin/js/templates/config.ts +147 -0
  13. package/src/admin/js/templates/errorRow.ts +21 -0
  14. package/src/admin/js/templates/index.ts +4 -0
  15. package/src/admin/js/templates/poolCard.ts +61 -0
  16. package/src/admin/js/templates/traceRow.ts +24 -0
  17. package/src/admin/js/tsconfig.json +24 -0
  18. package/src/admin/js/types.ts +336 -0
  19. package/src/admin/js/ui/components.ts +44 -0
  20. package/src/admin/js/ui/table.ts +173 -0
  21. package/src/admin/js/ui/theme.ts +93 -0
  22. package/src/admin/js/ui/views.ts +427 -0
  23. package/src/admin/js/util/format.ts +46 -0
  24. package/src/admin/js/util/metrics.ts +24 -0
  25. package/src/admin/lib/chart.bundle.css +1 -0
  26. package/src/admin/lib/chart.bundle.js +146 -0
  27. package/src/admin/style.css +1437 -0
  28. package/src/bin/load-test.js +202 -0
  29. package/src/handler/handlerAdmin.js +293 -0
  30. package/src/handler/plsql/cgi.js +1 -1
  31. package/src/handler/plsql/errorPage.js +31 -6
  32. package/src/handler/plsql/handlerPlSql.js +32 -6
  33. package/src/handler/plsql/owaPageStream.js +93 -0
  34. package/src/handler/plsql/parsePage.js +7 -3
  35. package/src/handler/plsql/procedure.js +221 -105
  36. package/src/handler/plsql/procedureNamed.js +34 -49
  37. package/src/handler/plsql/procedureSanitize.js +120 -45
  38. package/src/handler/plsql/procedureVariable.js +4 -2
  39. package/src/handler/plsql/request.js +8 -3
  40. package/src/handler/plsql/sendResponse.js +43 -5
  41. package/src/index.js +0 -1
  42. package/src/server/adminContext.js +23 -0
  43. package/src/server/config.js +1 -0
  44. package/src/server/server.js +130 -9
  45. package/src/types.js +7 -1
  46. package/src/util/cache.js +123 -0
  47. package/src/util/jsonLogger.js +47 -0
  48. package/src/util/shutdown.js +6 -2
  49. package/src/util/statsManager.js +368 -0
  50. package/src/util/trace.js +7 -6
  51. package/src/util/traceManager.js +68 -0
  52. package/src/version.js +1 -1
  53. package/types/admin/js/schemas.d.ts +384 -0
  54. package/types/admin/js/types.d.ts +312 -0
  55. package/types/handler/handlerAdmin.d.ts +79 -0
  56. package/types/handler/plsql/errorPage.d.ts +1 -0
  57. package/types/handler/plsql/handlerPlSql.d.ts +6 -1
  58. package/types/handler/plsql/owaPageStream.d.ts +28 -0
  59. package/types/handler/plsql/procedure.d.ts +4 -1
  60. package/types/handler/plsql/procedureNamed.d.ts +2 -5
  61. package/types/handler/plsql/procedureSanitize.d.ts +2 -5
  62. package/types/handler/plsql/procedureVariable.d.ts +1 -1
  63. package/types/handler/plsql/request.d.ts +3 -1
  64. package/types/handler/plsql/sendResponse.d.ts +1 -1
  65. package/types/index.d.ts +0 -1
  66. package/types/server/adminContext.d.ts +17 -0
  67. package/types/server/server.d.ts +5 -0
  68. package/types/types.d.ts +19 -1
  69. package/types/util/cache.d.ts +69 -0
  70. package/types/util/jsonLogger.d.ts +45 -0
  71. package/types/util/statsManager.d.ts +395 -0
  72. package/types/util/traceManager.d.ts +35 -0
  73. package/src/handler/handlerMetrics.js +0 -68
  74. package/types/handler/handlerMetrics.d.ts +0 -25
@@ -0,0 +1,368 @@
1
+ import debugModule from 'debug';
2
+ import os from 'node:os';
3
+
4
+ const debug = debugModule('webplsql:statsManager');
5
+
6
+ /**
7
+ * @typedef {object} StatsConfig
8
+ * @property {number} intervalMs - Duration of each statistical bucket (default: 5000ms).
9
+ * @property {number} maxHistoryPoints - Number of buckets to keep in the ring buffer (default: 200).
10
+ * @property {boolean} sampleSystem - Whether to automatically sample CPU/Memory (default: true).
11
+ * @property {boolean} samplePools - Whether to automatically sample Oracle pool utilization (default: true).
12
+ * @property {number} percentilePrecision - Max number of samples per bucket for P95/P99 calculation (default: 1000).
13
+ */
14
+
15
+ /**
16
+ * @typedef {object} CacheStats
17
+ * @property {number} size - Number of entries.
18
+ * @property {number} hits - Number of hits.
19
+ * @property {number} misses - Number of misses.
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} PoolCacheSnapshot
24
+ * @property {CacheStats} procedureName - Procedure name cache stats.
25
+ * @property {CacheStats} argument - Argument cache stats.
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} PoolSnapshot
30
+ * @property {string} name - The pool name.
31
+ * @property {number} connectionsInUse - Number of active connections.
32
+ * @property {number} connectionsOpen - Number of open connections.
33
+ * @property {PoolCacheSnapshot} [cache] - Cache statistics.
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} Bucket
38
+ * @property {number} timestamp - End time of the bucket.
39
+ * @property {number} requests - Number of requests.
40
+ * @property {number} errors - Number of errors.
41
+ * @property {number} durationMin - Minimum duration.
42
+ * @property {number} durationMax - Maximum duration.
43
+ * @property {number} durationAvg - Average duration.
44
+ * @property {number} durationP95 - 95th percentile duration.
45
+ * @property {number} durationP99 - 99th percentile duration.
46
+ * @property {object} system - System metrics.
47
+ * @property {number} system.cpu - CPU usage percentage.
48
+ * @property {number} system.heapUsed - Heap used in bytes.
49
+ * @property {number} system.heapTotal - Heap total in bytes.
50
+ * @property {number} system.rss - RSS in bytes.
51
+ * @property {number} system.external - External memory in bytes.
52
+ * @property {PoolSnapshot[]} pools - Pool utilization snapshots.
53
+ */
54
+
55
+ /**
56
+ * @typedef {object} CurrentBucket
57
+ * @property {number} count - Number of requests.
58
+ * @property {number} errors - Number of errors.
59
+ * @property {number} durationSum - Sum of durations.
60
+ * @property {number} durationMin - Minimum duration.
61
+ * @property {number} durationMax - Maximum duration.
62
+ * @property {number[]} durations - List of durations for percentile calculation.
63
+ */
64
+
65
+ /**
66
+ * @typedef {object} MemoryLifetime
67
+ * @property {number} heapUsedMax - Max heap used.
68
+ * @property {number} heapTotalMax - Max heap total.
69
+ * @property {number} rssMax - Max RSS.
70
+ * @property {number} externalMax - Max external.
71
+ */
72
+
73
+ /**
74
+ * @typedef {object} LifetimeStats
75
+ * @property {number} totalRequests - Total requests.
76
+ * @property {number} totalErrors - Total errors.
77
+ * @property {number} minDuration - Min duration.
78
+ * @property {number} maxDuration - Max duration.
79
+ * @property {number} totalDuration - Total duration.
80
+ * @property {number} maxRequestsPerSecond - Max requests per second.
81
+ * @property {MemoryLifetime} memory - Memory extremes.
82
+ * @property {object} cpu - CPU extremes.
83
+ * @property {number} cpu.max - Max CPU.
84
+ * @property {number} cpu.userMax - Max user CPU.
85
+ * @property {number} cpu.systemMax - Max system CPU.
86
+ */
87
+
88
+ /**
89
+ * @typedef {object} StatsSummary
90
+ * @property {Date} startTime - Server start time.
91
+ * @property {number} totalRequests - Total requests handled.
92
+ * @property {number} totalErrors - Total errors encountered.
93
+ * @property {number} avgResponseTime - Lifetime average response time.
94
+ * @property {number} minResponseTime - Lifetime minimum response time.
95
+ * @property {number} maxResponseTime - Lifetime maximum response time.
96
+ * @property {number} maxRequestsPerSecond - Lifetime maximum requests per second.
97
+ * @property {MemoryLifetime} maxMemory - Lifetime memory extremes.
98
+ * @property {object} cpu - CPU extremes.
99
+ * @property {number} cpu.max - Max CPU usage percentage.
100
+ * @property {number} cpu.userMax - Max user CPU usage in microseconds.
101
+ * @property {number} cpu.systemMax - Max system CPU usage in microseconds.
102
+ */
103
+
104
+ /**
105
+ * Manager for statistical data collection and temporal bucketing.
106
+ */
107
+ export class StatsManager {
108
+ /**
109
+ * @param {Partial<StatsConfig>} config - Configuration.
110
+ */
111
+ constructor(config = {}) {
112
+ /** @type {StatsConfig} */
113
+ this.config = {
114
+ intervalMs: 5000,
115
+ maxHistoryPoints: 1000,
116
+ sampleSystem: true,
117
+ samplePools: true,
118
+ percentilePrecision: 1000,
119
+ ...config,
120
+ };
121
+
122
+ this.startTime = new Date();
123
+
124
+ /** @type {Bucket[]} */
125
+ this.history = [];
126
+
127
+ /** @type {LifetimeStats} */
128
+ this.lifetime = {
129
+ totalRequests: 0,
130
+ totalErrors: 0,
131
+ minDuration: -1,
132
+ maxDuration: -1,
133
+ totalDuration: 0,
134
+ maxRequestsPerSecond: 0,
135
+ memory: {
136
+ heapUsedMax: 0,
137
+ heapTotalMax: 0,
138
+ rssMax: 0,
139
+ externalMax: 0,
140
+ },
141
+ cpu: {
142
+ max: 0,
143
+ userMax: 0,
144
+ systemMax: 0,
145
+ },
146
+ };
147
+
148
+ /** @type {CurrentBucket} */
149
+ this._currentBucket = {
150
+ count: 0,
151
+ errors: 0,
152
+ durations: [],
153
+ durationSum: 0,
154
+ durationMin: -1,
155
+ durationMax: -1,
156
+ };
157
+
158
+ this._lastCpuTimes = this._getSystemCpuTimes();
159
+
160
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
161
+ this._timer = undefined;
162
+ if (this.config.sampleSystem) {
163
+ this._timer = setInterval(() => {
164
+ this.rotateBucket();
165
+ }, this.config.intervalMs);
166
+ if (this._timer && typeof this._timer.unref === 'function') {
167
+ this._timer.unref();
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Reset the current bucket accumulator.
174
+ * @private
175
+ */
176
+ _resetBucket() {
177
+ this._currentBucket = {
178
+ count: 0,
179
+ errors: 0,
180
+ durations: [],
181
+ durationSum: 0,
182
+ durationMin: -1,
183
+ durationMax: -1,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Record a request event.
189
+ * @param {number} duration - Duration in milliseconds.
190
+ * @param {boolean} isError - Whether the request was an error.
191
+ */
192
+ recordRequest(duration, isError = false) {
193
+ this.lifetime.totalRequests++;
194
+ if (isError) {
195
+ this.lifetime.totalErrors++;
196
+ }
197
+
198
+ this.lifetime.totalDuration += duration;
199
+ if (this.lifetime.minDuration < 0 || duration < this.lifetime.minDuration) {
200
+ this.lifetime.minDuration = duration;
201
+ }
202
+ if (this.lifetime.maxDuration < 0 || duration > this.lifetime.maxDuration) {
203
+ this.lifetime.maxDuration = duration;
204
+ }
205
+
206
+ const b = this._currentBucket;
207
+ b.count++;
208
+ if (isError) {
209
+ b.errors++;
210
+ }
211
+
212
+ b.durationSum += duration;
213
+ if (b.durationMin < 0 || duration < b.durationMin) {
214
+ b.durationMin = duration;
215
+ }
216
+ if (b.durationMax < 0 || duration > b.durationMax) {
217
+ b.durationMax = duration;
218
+ }
219
+
220
+ if (b.durations.length < this.config.percentilePrecision) {
221
+ b.durations.push(duration);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Get system CPU times.
227
+ * @private
228
+ * @returns {{user: number, nice: number, sys: number, idle: number, irq: number, total: number}} System CPU times.
229
+ */
230
+ _getSystemCpuTimes() {
231
+ const cpus = os.cpus();
232
+ let user = 0;
233
+ let nice = 0;
234
+ let sys = 0;
235
+ let idle = 0;
236
+ let irq = 0;
237
+
238
+ for (const cpu of cpus) {
239
+ user += cpu.times.user;
240
+ nice += cpu.times.nice;
241
+ sys += cpu.times.sys;
242
+ idle += cpu.times.idle;
243
+ irq += cpu.times.irq;
244
+ }
245
+
246
+ const total = user + nice + sys + idle + irq;
247
+ return {user, nice, sys, idle, irq, total};
248
+ }
249
+
250
+ /**
251
+ * Calculate CPU usage percentage since last call.
252
+ * @private
253
+ * @returns {number} CPU usage percentage (0-100).
254
+ */
255
+ _calculateCpuUsage() {
256
+ const current = this._getSystemCpuTimes();
257
+ const last = this._lastCpuTimes || {user: 0, nice: 0, sys: 0, idle: 0, irq: 0, total: 0};
258
+
259
+ const deltaTotal = current.total - last.total;
260
+ const deltaIdle = current.idle - last.idle;
261
+
262
+ this._lastCpuTimes = current;
263
+
264
+ if (deltaTotal <= 0) return 0;
265
+
266
+ const percent = ((deltaTotal - deltaIdle) / deltaTotal) * 100;
267
+ return Math.min(100, Math.max(0, percent));
268
+ }
269
+
270
+ /**
271
+ * Rotate the current bucket into history and start a new one.
272
+ * @param {PoolSnapshot[]} [poolSnapshots] - Optional pool snapshots to include.
273
+ */
274
+ rotateBucket(poolSnapshots = []) {
275
+ const b = this._currentBucket;
276
+ const memUsage = process.memoryUsage();
277
+ const systemMemoryUsed = os.totalmem() - os.freemem();
278
+ const cpuUsage = process.cpuUsage();
279
+ const cpu = this._calculateCpuUsage();
280
+
281
+ // Update lifetime extremes
282
+ const reqPerSec = b.count / (this.config.intervalMs / 1000);
283
+ this.lifetime.maxRequestsPerSecond = Math.max(this.lifetime.maxRequestsPerSecond, reqPerSec);
284
+ this.lifetime.memory.heapUsedMax = Math.max(this.lifetime.memory.heapUsedMax, memUsage.heapUsed);
285
+ this.lifetime.memory.heapTotalMax = Math.max(this.lifetime.memory.heapTotalMax, memUsage.heapTotal);
286
+ this.lifetime.memory.rssMax = Math.max(this.lifetime.memory.rssMax, systemMemoryUsed);
287
+ this.lifetime.memory.externalMax = Math.max(this.lifetime.memory.externalMax, memUsage.external);
288
+ this.lifetime.cpu.max = Math.max(this.lifetime.cpu.max, cpu);
289
+ this.lifetime.cpu.userMax = Math.max(this.lifetime.cpu.userMax, cpuUsage.user);
290
+ this.lifetime.cpu.systemMax = Math.max(this.lifetime.cpu.systemMax, cpuUsage.system);
291
+
292
+ let p95 = 0;
293
+ let p99 = 0;
294
+ if (b.durations.length > 0) {
295
+ const sorted = [...b.durations].sort((x, y) => x - y);
296
+ const p95Idx = Math.floor(sorted.length * 0.95);
297
+ const p99Idx = Math.floor(sorted.length * 0.99);
298
+ const lastIdx = sorted.length - 1;
299
+
300
+ p95 = sorted[p95Idx] ?? sorted[lastIdx] ?? 0;
301
+ p99 = sorted[p99Idx] ?? sorted[lastIdx] ?? 0;
302
+ }
303
+
304
+ /** @type {Bucket} */
305
+ const bucket = {
306
+ timestamp: Date.now(),
307
+ requests: b.count,
308
+ errors: b.errors,
309
+ durationMin: b.durationMin < 0 ? 0 : b.durationMin,
310
+ durationMax: b.durationMax < 0 ? 0 : b.durationMax,
311
+ durationAvg: b.count > 0 ? b.durationSum / b.count : 0,
312
+ durationP95: p95,
313
+ durationP99: p99,
314
+ system: {
315
+ cpu,
316
+ heapUsed: memUsage.heapUsed,
317
+ heapTotal: memUsage.heapTotal,
318
+ rss: systemMemoryUsed,
319
+ external: memUsage.external,
320
+ },
321
+ pools: poolSnapshots,
322
+ };
323
+
324
+ this.history.push(bucket);
325
+ if (this.history.length > this.config.maxHistoryPoints) {
326
+ this.history.shift();
327
+ }
328
+
329
+ this._resetBucket();
330
+ debug('Bucket rotated: %j', bucket);
331
+ }
332
+
333
+ /**
334
+ * Stop the background timer.
335
+ */
336
+ stop() {
337
+ if (this._timer) {
338
+ clearInterval(this._timer);
339
+ this._timer = undefined;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get lifetime summary.
345
+ * @returns {StatsSummary} Summary.
346
+ */
347
+ getSummary() {
348
+ return {
349
+ startTime: this.startTime,
350
+ totalRequests: this.lifetime.totalRequests,
351
+ totalErrors: this.lifetime.totalErrors,
352
+ avgResponseTime: this.lifetime.totalRequests > 0 ? this.lifetime.totalDuration / this.lifetime.totalRequests : 0,
353
+ minResponseTime: this.lifetime.minDuration,
354
+ maxResponseTime: this.lifetime.maxDuration,
355
+ maxRequestsPerSecond: this.lifetime.maxRequestsPerSecond,
356
+ maxMemory: this.lifetime.memory,
357
+ cpu: this.lifetime.cpu,
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Get history buffer.
363
+ * @returns {Bucket[]} The history buffer.
364
+ */
365
+ getHistory() {
366
+ return this.history;
367
+ }
368
+ }
package/src/util/trace.js CHANGED
@@ -57,7 +57,7 @@ export const toTable = (head, body) => {
57
57
 
58
58
  // Calculate column widths
59
59
  const widths = head.map((h, i) => {
60
- const bodyMax = Math.max(0, ...body.map((row) => (row[i] || '').length));
60
+ const bodyMax = Math.max(0, ...body.map((row) => (row[i] ?? '').length));
61
61
  return Math.max(h.length, bodyMax);
62
62
  });
63
63
 
@@ -68,13 +68,13 @@ export const toTable = (head, body) => {
68
68
  * @returns {string} - The result
69
69
  */
70
70
  const padCell = (cell, width) => cell.padEnd(width, ' ');
71
- const textHeader = head.map((h, i) => padCell(h, widths[i])).join(' | ');
72
- const textSeparator = widths.map((w) => '-'.repeat(w)).join('-+-');
73
- const textRows = body.map((row) => head.map((_, i) => padCell(row[i] || '', widths[i])).join(' | '));
71
+ const textHeader = head.map((h, i) => padCell(h, widths[i] ?? 0)).join(' | ');
72
+ const textSeparator = widths.map((w) => '-'.repeat(w ?? 0)).join('-+-');
73
+ const textRows = body.map((row) => head.map((_, i) => padCell(row[i] ?? '', widths[i] ?? 0)).join(' | '));
74
74
  const text = [textHeader, textSeparator, ...textRows].join('\n');
75
75
 
76
76
  const htmlHead = head.map((h) => `<th>${escapeHtml(h)}</th>`).join('');
77
- const htmlBody = body.map((row) => `<tr>${head.map((_, i) => `<td>${escapeHtml(row[i] || '')}</td>`).join('')}</tr>`).join('');
77
+ const htmlBody = body.map((row) => `<tr>${head.map((_, i) => `<td>${escapeHtml(row[i] ?? '')}</td>`).join('')}</tr>`).join('');
78
78
  const html = `<table><thead><tr>${htmlHead}</tr></thead><tbody>${htmlBody}</tbody></table>`;
79
79
 
80
80
  return {text, html};
@@ -279,7 +279,8 @@ export const getFormattedMessage = (para) => {
279
279
 
280
280
  // header
281
281
  const url = typeof para.req?.originalUrl === 'string' && para.req.originalUrl.length > 0 ? ` on ${para.req.originalUrl}` : '';
282
- const header = `${para.type.toUpperCase()} at ${timestamp.toUTCString()}${url}`;
282
+ const type = (para.type ?? 'trace').toUpperCase();
283
+ const header = `${type} at ${timestamp.toUTCString()}${url}`;
283
284
  const output = {
284
285
  html: `<h1>${header}</h1>`,
285
286
  text: `\n\n${SEPARATOR_H1}\n== ${header}\n${SEPARATOR_H1}\n`,
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * @typedef {import('../admin/js/types.js').TraceEntry} TraceEntry
6
+ */
7
+
8
+ class TraceManager {
9
+ constructor() {
10
+ this.enabled = false;
11
+ this.filename = 'trace.json.log';
12
+ this.maxEntries = 1000;
13
+ }
14
+
15
+ /**
16
+ * Toggle tracing
17
+ * @param {boolean} enabled - New state
18
+ */
19
+ setEnabled(enabled) {
20
+ this.enabled = enabled;
21
+ }
22
+
23
+ /**
24
+ * Is tracing enabled?
25
+ * @returns {boolean} - The state
26
+ */
27
+ isEnabled() {
28
+ return this.enabled;
29
+ }
30
+
31
+ /**
32
+ * Add a trace entry
33
+ * @param {object} entry - The trace entry
34
+ */
35
+ addTrace(entry) {
36
+ if (!this.enabled) return;
37
+
38
+ try {
39
+ const line = JSON.stringify(entry) + '\n';
40
+ fs.appendFileSync(this.filename, line);
41
+ } catch (err) {
42
+ console.error('TraceManager: error writing trace', err);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Clear all traces
48
+ */
49
+ clear() {
50
+ try {
51
+ if (fs.existsSync(this.filename)) {
52
+ fs.truncateSync(this.filename, 0);
53
+ }
54
+ } catch (err) {
55
+ console.error('TraceManager: error clearing traces', err);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get the full path to the trace file
61
+ * @returns {string} - The path
62
+ */
63
+ getFilePath() {
64
+ return path.resolve(this.filename);
65
+ }
66
+ }
67
+
68
+ export const traceManager = new TraceManager();
package/src/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  * Returns the current library version
3
3
  * @returns {string} - Version.
4
4
  */
5
- export const getVersion = () => '0.18.0';
5
+ export const getVersion = () => '1.2.0';