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,202 @@
1
+ /**
2
+ * @file Load testing CLI tool for web_plsql.
3
+ * This tool simulates parallel traffic to the middleware and provides real-time statistics.
4
+ */
5
+
6
+ import {performance} from 'node:perf_hooks';
7
+
8
+ /**
9
+ * @typedef {object} Metrics
10
+ * @property {number} total Total requests made.
11
+ * @property {number} success Number of successful requests (2xx).
12
+ * @property {number} error Number of failed requests.
13
+ * @property {number} minLatency Minimum latency in milliseconds.
14
+ * @property {number} maxLatency Maximum latency in milliseconds.
15
+ * @property {number} sumLatency Sum of all latencies for average calculation.
16
+ * @property {Map<string|number, number>} statusCodes Frequency of status codes.
17
+ */
18
+
19
+ /** @type {Metrics} */
20
+ const metrics = {
21
+ total: 0,
22
+ success: 0,
23
+ error: 0,
24
+ minLatency: Number.MAX_SAFE_INTEGER,
25
+ maxLatency: 0,
26
+ sumLatency: 0,
27
+ statusCodes: new Map(),
28
+ };
29
+
30
+ let stopped = false;
31
+
32
+ /**
33
+ * Parse CLI arguments.
34
+ * @returns {{ url: string, concurrency: number, duration: number }} Options object.
35
+ */
36
+ function parseArgs() {
37
+ const args = process.argv.slice(2);
38
+ const options = {
39
+ url: '',
40
+ concurrency: 10,
41
+ duration: 30,
42
+ };
43
+
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i];
46
+ const nextArg = args[i + 1] ?? '';
47
+
48
+ if (arg === '--url' || arg === '-u') {
49
+ options.url = nextArg;
50
+ i++;
51
+ } else if (arg === '--concurrency' || arg === '-c') {
52
+ options.concurrency = parseInt(nextArg, 10);
53
+ i++;
54
+ } else if (arg === '--duration' || arg === '-d') {
55
+ options.duration = parseInt(nextArg, 10);
56
+ i++;
57
+ }
58
+ }
59
+
60
+ if (!options.url) {
61
+ console.error('Usage: node src/bin/load-test.js --url <url> [-c concurrency] [-d duration]');
62
+ process.exit(1);
63
+ }
64
+
65
+ return options;
66
+ }
67
+
68
+ /**
69
+ * Record a single request's result.
70
+ * @param {boolean} ok Whether the request succeeded.
71
+ * @param {number} latency Latency in milliseconds.
72
+ * @param {number|string} status Status code or error string.
73
+ */
74
+ function recordMetrics(ok, latency, status) {
75
+ metrics.total++;
76
+ if (ok) {
77
+ metrics.success++;
78
+ } else {
79
+ metrics.error++;
80
+ }
81
+
82
+ metrics.minLatency = Math.min(metrics.minLatency, latency);
83
+ metrics.maxLatency = Math.max(metrics.maxLatency, latency);
84
+ metrics.sumLatency += latency;
85
+
86
+ const count = metrics.statusCodes.get(status) ?? 0;
87
+ metrics.statusCodes.set(status, count + 1);
88
+ }
89
+
90
+ /**
91
+ * Clear the console and move cursor to the top.
92
+ */
93
+ function clearConsole() {
94
+ process.stdout.write('\x1b[2J\x1b[0;0H');
95
+ }
96
+
97
+ /**
98
+ * Render the dashboard.
99
+ * @param {number} elapsed Elapsed time in seconds.
100
+ * @param {number} duration Total duration in seconds.
101
+ * @param {string} url Target URL.
102
+ * @param {number} concurrency Concurrency level.
103
+ */
104
+ function renderDashboard(elapsed, duration, url, concurrency) {
105
+ clearConsole();
106
+ const rps = elapsed > 0 ? (metrics.total / elapsed).toFixed(1) : '0.0';
107
+ const avgLatency = metrics.total > 0 ? (metrics.sumLatency / metrics.total).toFixed(1) : '0.0';
108
+ const progress = Math.min(100, (elapsed / duration) * 100).toFixed(1);
109
+ const barWidth = 30;
110
+ const filledWidth = Math.floor((barWidth * parseFloat(progress)) / 100);
111
+ const bar = '='.repeat(filledWidth) + '>'.padEnd(barWidth - filledWidth, ' ');
112
+
113
+ console.log('\x1b[1m\x1b[36mWeb PL/SQL Load Test\x1b[0m');
114
+ console.log('='.repeat(40));
115
+ console.log(`\x1b[33mURL:\x1b[0m ${url}`);
116
+ console.log(`\x1b[33mConcurrency:\x1b[0m ${concurrency}`);
117
+ console.log(`\x1b[33mDuration:\x1b[0m ${duration}s`);
118
+ console.log('='.repeat(40));
119
+ console.log(`\x1b[32mProgress:\x1b[0m [${bar}] ${progress}% (${elapsed.toFixed(1)}s)`);
120
+ console.log(`\x1b[32mRPS:\x1b[0m ${rps}`);
121
+ console.log(`\x1b[32mTotal:\x1b[0m ${metrics.total}`);
122
+ console.log(`\x1b[32mSuccess:\x1b[0m ${metrics.success}`);
123
+ console.log(`\x1b[31mErrors:\x1b[0m ${metrics.error}`);
124
+ console.log('='.repeat(40));
125
+ console.log('\x1b[1mLatency (ms)\x1b[0m');
126
+ console.log(`\x1b[34mAvg:\x1b[0m ${avgLatency.padStart(8)}`);
127
+ console.log(`\x1b[34mMin:\x1b[0m ${(metrics.minLatency === Number.MAX_SAFE_INTEGER ? 0 : metrics.minLatency).toFixed(1).padStart(8)}`);
128
+ console.log(`\x1b[34mMax:\x1b[0m ${metrics.maxLatency.toFixed(1).padStart(8)}`);
129
+ console.log('='.repeat(40));
130
+
131
+ if (metrics.statusCodes.size > 0) {
132
+ console.log('\x1b[1mStatus Codes\x1b[0m');
133
+ for (const [code, count] of metrics.statusCodes.entries()) {
134
+ const numericCode = typeof code === 'number' ? code : 0;
135
+ const color = numericCode >= 200 && numericCode < 300 ? '\x1b[32m' : '\x1b[31m';
136
+ console.log(`${color}${code}\x1b[0m: ${count}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Worker function to maintain concurrency.
143
+ * @param {string} url The target URL.
144
+ * @param {number} endTime The end time in milliseconds.
145
+ */
146
+ async function runWorker(url, endTime) {
147
+ while (performance.now() < endTime && !stopped) {
148
+ const start = performance.now();
149
+ try {
150
+ const res = await fetch(url, {
151
+ signal: AbortSignal.timeout(10000), // 10s timeout
152
+ });
153
+ const latency = performance.now() - start;
154
+ recordMetrics(res.ok, latency, res.status);
155
+ // Consume body to free up connection
156
+ await res.arrayBuffer();
157
+ } catch (err) {
158
+ const latency = performance.now() - start;
159
+ const errorName = err instanceof Error ? err.name : 'UnknownError';
160
+ recordMetrics(false, latency, errorName === 'TimeoutError' ? 'Timeout' : 'NetworkErr');
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Main function.
167
+ */
168
+ async function main() {
169
+ const options = parseArgs();
170
+ const startTime = performance.now();
171
+ const endTime = startTime + options.duration * 1000;
172
+
173
+ console.log(`Starting load test on ${options.url}...`);
174
+
175
+ // Start workers
176
+ const workers = Array.from({length: options.concurrency}, () => runWorker(options.url, endTime));
177
+
178
+ // Dashboard refresh interval
179
+ const uiInterval = setInterval(() => {
180
+ const elapsed = (performance.now() - startTime) / 1000;
181
+ renderDashboard(elapsed, options.duration, options.url, options.concurrency);
182
+ }, 200);
183
+
184
+ // Handle Ctrl+C
185
+ process.on('SIGINT', () => {
186
+ stopped = true;
187
+ console.log('\nStopping...');
188
+ });
189
+
190
+ await Promise.all(workers);
191
+ clearInterval(uiInterval);
192
+
193
+ // Final render
194
+ const finalElapsed = (performance.now() - startTime) / 1000;
195
+ renderDashboard(finalElapsed, options.duration, options.url, options.concurrency);
196
+ console.log('\n\x1b[1m\x1b[32mTest Complete!\x1b[0m\n');
197
+ }
198
+
199
+ main().catch((/** @type {unknown} */ err) => {
200
+ console.error('Fatal error:', err);
201
+ process.exit(1);
202
+ });
@@ -0,0 +1,293 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import readline from 'node:readline';
5
+ import {AdminContext} from '../server/adminContext.js';
6
+ import {traceManager} from '../util/traceManager.js';
7
+ import {getVersion} from '../version.js';
8
+
9
+ const version = getVersion();
10
+ import {forceShutdown} from '../util/shutdown.js';
11
+
12
+ /**
13
+ * @typedef {import('express').Request} Request
14
+ * @typedef {import('express').Response} Response
15
+ * @typedef {import('express').NextFunction} NextFunction
16
+ */
17
+
18
+ /**
19
+ * @typedef {import('../util/statsManager.js').Bucket} Bucket
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} StatsSummary
24
+ * @property {Date} startTime - Server start time.
25
+ * @property {number} totalRequests - Total requests handled.
26
+ * @property {number} totalErrors - Total errors encountered.
27
+ * @property {number} avgResponseTime - Lifetime average response time.
28
+ * @property {number} minResponseTime - Lifetime minimum response time.
29
+ * @property {number} maxResponseTime - Lifetime maximum response time.
30
+ * @property {number} maxRequestsPerSecond - Lifetime maximum requests per second.
31
+ * @property {object} maxMemory - Lifetime memory extremes.
32
+ * @property {number} maxMemory.heapUsedMax - Maximum heap used.
33
+ * @property {number} maxMemory.heapTotalMax - Maximum heap total.
34
+ * @property {number} maxMemory.rssMax - Maximum RSS.
35
+ * @property {number} maxMemory.externalMax - Maximum external memory.
36
+ * @property {object} cpu - Lifetime CPU extremes.
37
+ * @property {number} cpu.max - Max CPU.
38
+ * @property {number} cpu.userMax - Max user CPU.
39
+ * @property {number} cpu.systemMax - Max system CPU.
40
+ */
41
+
42
+ export const handlerAdmin = express.Router();
43
+
44
+ /**
45
+ * Helper to read last N lines of a file
46
+ * @param {string} filePath - Path to file
47
+ * @param {number} n - Number of lines
48
+ * @param {string} [filter] - Optional filter string
49
+ * @returns {Promise<string[]>} - The lines
50
+ */
51
+ const readLastLines = async (filePath, n = 100, filter = '') => {
52
+ if (!fs.existsSync(filePath)) {
53
+ return [];
54
+ }
55
+
56
+ const fileStream = fs.createReadStream(filePath);
57
+ const rl = readline.createInterface({
58
+ input: fileStream,
59
+ crlfDelay: Infinity,
60
+ });
61
+
62
+ const filterLower = filter.toLowerCase();
63
+
64
+ /** @type {string[]} */
65
+ const lines = [];
66
+ for await (const line of rl) {
67
+ if (!filter || line.toLowerCase().includes(filterLower)) {
68
+ lines.push(line);
69
+ if (lines.length > n) {
70
+ lines.shift();
71
+ }
72
+ }
73
+ }
74
+ return lines;
75
+ };
76
+
77
+ // GET /api/status
78
+ handlerAdmin.get('/api/status', (_req, res) => {
79
+ const uptime = (new Date().getTime() - AdminContext.startTime.getTime()) / 1000;
80
+
81
+ const poolStats = AdminContext.pools.map((pool, index) => {
82
+ const cache = AdminContext.caches[index];
83
+ const name = cache?.poolName ?? `pool-${index}`;
84
+ const p = /** @type {import('oracledb').Pool & {getStatistics?: () => Record<string, unknown>}} */ (pool);
85
+ const procStats = cache?.procedureNameCache.getStats();
86
+ const argStats = cache?.argumentCache.getStats();
87
+
88
+ return {
89
+ name,
90
+ stats: typeof p.getStatistics === 'function' ? p.getStatistics() : null,
91
+ connectionsOpen: pool.connectionsOpen,
92
+ connectionsInUse: pool.connectionsInUse,
93
+ cache: {
94
+ procedureName: {
95
+ size: cache?.procedureNameCache.keys().length ?? 0,
96
+ hits: procStats?.hits ?? 0,
97
+ misses: procStats?.misses ?? 0,
98
+ },
99
+ argument: {
100
+ size: cache?.argumentCache.keys().length ?? 0,
101
+ hits: argStats?.hits ?? 0,
102
+ misses: argStats?.misses ?? 0,
103
+ },
104
+ },
105
+ };
106
+ });
107
+
108
+ const memUsage = process.memoryUsage();
109
+ const systemMemoryUsed = os.totalmem() - os.freemem();
110
+ const cpuUsage = process.cpuUsage();
111
+ const summary = /** @type {StatsSummary} */ (AdminContext.statsManager.getSummary());
112
+
113
+ res.json({
114
+ version,
115
+ status: AdminContext.paused ? 'paused' : 'running',
116
+ uptime,
117
+ startTime: AdminContext.startTime,
118
+ intervalMs: AdminContext.statsManager.config.intervalMs,
119
+ metrics: {
120
+ requestCount: summary.totalRequests,
121
+ errorCount: summary.totalErrors,
122
+ avgResponseTime: summary.avgResponseTime,
123
+ minResponseTime: summary.minResponseTime,
124
+ maxResponseTime: summary.maxResponseTime,
125
+ maxRequestsPerSecond: summary.maxRequestsPerSecond,
126
+ },
127
+ history: AdminContext.statsManager.getHistory(),
128
+ pools: poolStats,
129
+ system: {
130
+ nodeVersion: process.version,
131
+ platform: process.platform,
132
+ arch: process.arch,
133
+ cpuCores: os.cpus().length,
134
+ memory: {
135
+ rss: systemMemoryUsed,
136
+ heapTotal: memUsage.heapTotal,
137
+ heapUsed: memUsage.heapUsed,
138
+ external: memUsage.external,
139
+ totalMemory: os.totalmem(),
140
+ ...summary.maxMemory,
141
+ },
142
+ cpu: {
143
+ user: cpuUsage.user,
144
+ system: cpuUsage.system,
145
+ max: summary.cpu.max,
146
+ userMax: summary.cpu.userMax,
147
+ systemMax: summary.cpu.systemMax,
148
+ },
149
+ },
150
+ config: AdminContext.config
151
+ ? {
152
+ ...AdminContext.config,
153
+ adminPassword: AdminContext.config.adminPassword ? '********' : undefined,
154
+ routePlSql: AdminContext.config.routePlSql.map((p) => ({
155
+ ...p,
156
+ password: '********',
157
+ })),
158
+ }
159
+ : null,
160
+ });
161
+ });
162
+
163
+ // GET /api/logs/error
164
+ handlerAdmin.get('/api/logs/error', async (req, res) => {
165
+ try {
166
+ const limit = Number(req.query.limit) || 100;
167
+ const filter = typeof req.query.filter === 'string' ? req.query.filter : '';
168
+ const logFile = 'error.json.log';
169
+ const lines = await readLastLines(logFile, limit, filter);
170
+
171
+ /** @type {Record<string, unknown>[]} */
172
+ const logs = [];
173
+ for (const line of lines) {
174
+ try {
175
+ const parsed = /** @type {unknown} */ (JSON.parse(line));
176
+ if (parsed && typeof parsed === 'object') {
177
+ logs.push(/** @type {Record<string, unknown>} */ (parsed));
178
+ }
179
+ } catch (e) {
180
+ // ignore
181
+ }
182
+ }
183
+
184
+ res.json(logs.reverse());
185
+ } catch (err) {
186
+ res.status(500).json({error: String(err)});
187
+ }
188
+ });
189
+
190
+ // GET /api/logs/access
191
+ handlerAdmin.get('/api/logs/access', async (req, res) => {
192
+ try {
193
+ const limit = Number(req.query.limit) || 100;
194
+ const filter = typeof req.query.filter === 'string' ? req.query.filter : '';
195
+ const logFile = AdminContext.config?.loggerFilename ?? 'access.log';
196
+
197
+ if (!AdminContext.config?.loggerFilename) {
198
+ res.json({message: 'Access logging not enabled'});
199
+ return;
200
+ }
201
+
202
+ const lines = await readLastLines(logFile, limit, filter);
203
+ res.json(lines.reverse());
204
+ } catch (err) {
205
+ res.status(500).json({error: String(err)});
206
+ }
207
+ });
208
+
209
+ // GET /api/cache - REMOVED (Merged into /api/status)
210
+
211
+ // POST /api/cache/clear
212
+ handlerAdmin.post('/api/cache/clear', (req, res) => {
213
+ const body = /** @type {unknown} */ (req.body);
214
+ const poolName = body && typeof body === 'object' && 'poolName' in body && typeof body.poolName === 'string' ? body.poolName : undefined;
215
+
216
+ let cleared = 0;
217
+ AdminContext.caches.forEach((c) => {
218
+ if (!poolName || c.poolName === poolName) {
219
+ c.procedureNameCache.clear();
220
+ cleared++;
221
+ c.argumentCache.clear();
222
+ cleared++;
223
+ }
224
+ });
225
+
226
+ res.json({message: `Cleared ${cleared} caches`});
227
+ });
228
+
229
+ // POST /api/server/:action
230
+ handlerAdmin.post('/api/server/:action', (req, res) => {
231
+ const action = req.params.action;
232
+
233
+ if (action === 'stop') {
234
+ res.json({message: 'Server shutting down...'});
235
+ setTimeout(() => {
236
+ forceShutdown();
237
+ }, 100);
238
+ } else if (action === 'pause') {
239
+ AdminContext.paused = true;
240
+ res.json({message: 'Server paused', status: 'paused'});
241
+ } else if (action === 'resume') {
242
+ AdminContext.paused = false;
243
+ res.json({message: 'Server resumed', status: 'running'});
244
+ } else {
245
+ res.status(400).json({error: 'Invalid action'});
246
+ }
247
+ });
248
+
249
+ // GET /api/trace/status
250
+ handlerAdmin.get('/api/trace/status', (_req, res) => {
251
+ res.json({enabled: traceManager.isEnabled()});
252
+ });
253
+
254
+ // POST /api/trace/toggle
255
+ handlerAdmin.post('/api/trace/toggle', (req, res) => {
256
+ const body = /** @type {unknown} */ (req.body);
257
+ const enabled = body && typeof body === 'object' && 'enabled' in body && typeof body.enabled === 'boolean' ? body.enabled : false;
258
+ traceManager.setEnabled(enabled);
259
+ res.json({enabled: traceManager.isEnabled()});
260
+ });
261
+
262
+ // GET /api/trace/logs
263
+ handlerAdmin.get('/api/trace/logs', async (req, res) => {
264
+ try {
265
+ const limit = Number(req.query.limit) || 100;
266
+ const filter = typeof req.query.filter === 'string' ? req.query.filter : '';
267
+ const logFile = traceManager.getFilePath();
268
+ const lines = await readLastLines(logFile, limit, filter);
269
+
270
+ /** @type {Record<string, unknown>[]} */
271
+ const logs = [];
272
+ for (const line of lines) {
273
+ try {
274
+ const parsed = /** @type {unknown} */ (JSON.parse(line));
275
+ if (parsed && typeof parsed === 'object') {
276
+ logs.push(/** @type {Record<string, unknown>} */ (parsed));
277
+ }
278
+ } catch (e) {
279
+ // ignore
280
+ }
281
+ }
282
+
283
+ res.json(logs.reverse());
284
+ } catch (err) {
285
+ res.status(500).json({error: String(err)});
286
+ }
287
+ });
288
+
289
+ // POST /api/trace/clear
290
+ handlerAdmin.post('/api/trace/clear', (_req, res) => {
291
+ traceManager.clear();
292
+ res.json({message: 'Traces cleared'});
293
+ });
@@ -109,7 +109,7 @@ export const getCGI = (req, doctable, cgi) => {
109
109
  const CGI = {
110
110
  SERVER_PORT: typeof req.socket.localPort === 'number' ? req.socket.localPort.toString() : '',
111
111
  REQUEST_METHOD: req.method,
112
- PATH_INFO: Array.isArray(req.params.name) ? req.params.name[0] : req.params.name,
112
+ PATH_INFO: Array.isArray(req.params.name) ? (req.params.name[0] ?? '') : (req.params.name ?? ''),
113
113
  SCRIPT_NAME: PATH.script,
114
114
  REMOTE_ADDR: (req.ip ?? '').replace('::ffff:', ''),
115
115
  SERVER_PROTOCOL: `${PROTOCOL}/${req.httpVersion}`,
@@ -7,6 +7,7 @@ import {RequestError} from './requestError.js';
7
7
  import {getFormattedMessage, logToFile} from '../../util/trace.js';
8
8
  import {errorToString} from '../../util/errorToString.js';
9
9
  import {getHtmlPage} from '../../util/html.js';
10
+ import {jsonLogger} from '../../util/jsonLogger.js';
10
11
 
11
12
  /**
12
13
  * @typedef {import('express').Request} Request
@@ -15,15 +16,16 @@ import {getHtmlPage} from '../../util/html.js';
15
16
  * @typedef {import('../../types.js').environmentType} environmentType
16
17
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
17
18
  * @typedef {{html: string; text: string}} outputType
19
+ * @typedef {import('../../util/trace.js').messageType} messageType
18
20
  */
19
21
 
20
22
  /**
21
- * Show an error page
23
+ * Get error data
22
24
  * @param {Request} req - The req object represents the HTTP request.
23
25
  * @param {unknown} error - The error.
24
- * @returns {outputType} - The output.
26
+ * @returns {messageType} - The output.
25
27
  */
26
- const getError = (req, error) => {
28
+ const getErrorData = (req, error) => {
27
29
  let timestamp = new Date();
28
30
  let message = '';
29
31
  /** @type {environmentType | null} */
@@ -58,7 +60,7 @@ const getError = (req, error) => {
58
60
  /* v8 ignore stop */
59
61
  }
60
62
 
61
- return getFormattedMessage({type: 'error', timestamp, message, req, environment, sql, bind});
63
+ return {type: 'error', timestamp, message, req, environment, sql, bind};
62
64
  };
63
65
 
64
66
  /**
@@ -70,12 +72,35 @@ const getError = (req, error) => {
70
72
  * @param {unknown} error - The error.
71
73
  */
72
74
  export const errorPage = (req, res, options, error) => {
73
- // get error message
74
- const {html, text} = getError(req, error);
75
+ // get error data
76
+ const errorData = getErrorData(req, error);
77
+
78
+ // get formatted message
79
+ const {html, text} = getFormattedMessage(errorData);
75
80
 
76
81
  // trace to file
77
82
  logToFile(text);
78
83
 
84
+ // json log
85
+ const firstLine = errorData.message.split('\n')[0];
86
+ jsonLogger.log({
87
+ timestamp: errorData.timestamp?.toISOString() ?? new Date().toISOString(),
88
+ type: 'error',
89
+ message: firstLine ?? '',
90
+ req: {
91
+ method: req.method,
92
+ url: req.originalUrl,
93
+ ip: req.ip ?? '',
94
+ userAgent: req.get('user-agent') ?? '',
95
+ },
96
+ details: {
97
+ fullMessage: errorData.message,
98
+ sql: errorData.sql,
99
+ bind: errorData.bind,
100
+ environment: errorData.environment,
101
+ },
102
+ });
103
+
79
104
  // console
80
105
  console.error(text);
81
106
 
@@ -9,6 +9,7 @@ import url from 'node:url';
9
9
  import {processRequest} from './request.js';
10
10
  import {RequestError} from './requestError.js';
11
11
  import {errorPage} from './errorPage.js';
12
+ import {Cache} from '../../util/cache.js';
12
13
 
13
14
  /**
14
15
  * @typedef {import('express').RequestHandler} RequestHandler
@@ -24,11 +25,13 @@ import {errorPage} from './errorPage.js';
24
25
  * express.Request handler
25
26
  * @param {Request} req - The req object represents the HTTP request.
26
27
  * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
27
- * @param {NextFunction} next - The next function.
28
+ * @param {NextFunction} _next - The next function.
28
29
  * @param {Pool} connectionPool - The connection pool.
29
30
  * @param {configPlSqlHandlerType} options - the options for the middleware.
31
+ * @param {Cache<string>} procedureNameCache - The procedure name cache.
32
+ * @param {Cache<import('./procedureNamed.js').argsType>} argumentCache - The argument cache.
30
33
  */
31
- const requestHandler = async (req, res, next, connectionPool, options) => {
34
+ const requestHandler = async (req, res, _next, connectionPool, options, procedureNameCache, argumentCache) => {
32
35
  try {
33
36
  // should we switch to the default page if there is one defined
34
37
  if (typeof req.params.name !== 'string' || req.params.name.length === 0) {
@@ -41,24 +44,47 @@ const requestHandler = async (req, res, next, connectionPool, options) => {
41
44
  }
42
45
  } else {
43
46
  // request handler
44
- await processRequest(req, res, options, connectionPool);
47
+ await processRequest(req, res, options, connectionPool, procedureNameCache, argumentCache);
45
48
  }
46
49
  } catch (err) {
47
50
  errorPage(req, res, options, err);
48
51
  }
49
52
  };
50
53
 
54
+ /**
55
+ * @typedef {import('express').RequestHandler & {
56
+ * procedureNameCache: Cache<string>;
57
+ * argumentCache: Cache<import('./procedureNamed.js').argsType>;
58
+ * }} WebPlSqlRequestHandler
59
+ */
60
+
51
61
  /**
52
62
  * Express middleware.
53
63
  *
54
64
  * @param {Pool} connectionPool - The connection pool.
55
65
  * @param {configPlSqlHandlerType} config - The configuration options.
56
- * @returns {RequestHandler} - The handler.
66
+ * @returns {WebPlSqlRequestHandler} - The handler.
57
67
  */
58
68
  export const handlerWebPlSql = (connectionPool, config) => {
59
69
  debug('options', config);
60
70
 
61
- return (req, res, next) => {
62
- void requestHandler(req, res, next, connectionPool, config);
71
+ /** @type {Cache<string>} */
72
+ const procedureNameCache = new Cache();
73
+ /** @type {Cache<import('./procedureNamed.js').argsType>} */
74
+ const argumentCache = new Cache();
75
+
76
+ /**
77
+ * @param {Request} req - The request.
78
+ * @param {Response} res - The response.
79
+ * @param {NextFunction} next - The next function.
80
+ */
81
+ const handler = (req, res, next) => {
82
+ void requestHandler(req, res, next, connectionPool, config, procedureNameCache, argumentCache);
63
83
  };
84
+
85
+ // Expose caches for Admin Console
86
+ handler.procedureNameCache = procedureNameCache;
87
+ handler.argumentCache = argumentCache;
88
+
89
+ return handler;
64
90
  };