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.
- package/README.md +29 -9
- package/package.json +17 -7
- package/src/admin/client/charts.ts +598 -0
- package/src/admin/client/main.js +3 -0
- package/src/admin/client/tailwind.css +41 -0
- package/src/admin/favicon.svg +3 -0
- package/src/admin/index.html +529 -0
- package/src/admin/js/api.ts +170 -0
- package/src/admin/js/app.ts +614 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +215 -0
- package/src/admin/js/templates/config.ts +147 -0
- package/src/admin/js/templates/errorRow.ts +21 -0
- package/src/admin/js/templates/index.ts +4 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/templates/traceRow.ts +24 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +336 -0
- package/src/admin/js/ui/components.ts +44 -0
- package/src/admin/js/ui/table.ts +173 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +427 -0
- package/src/admin/js/util/format.ts +46 -0
- package/src/admin/js/util/metrics.ts +24 -0
- package/src/admin/lib/chart.bundle.css +1 -0
- package/src/admin/lib/chart.bundle.js +146 -0
- package/src/admin/style.css +1437 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +293 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +31 -6
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/owaPageStream.js +93 -0
- package/src/handler/plsql/parsePage.js +7 -3
- package/src/handler/plsql/procedure.js +221 -105
- package/src/handler/plsql/procedureNamed.js +34 -49
- package/src/handler/plsql/procedureSanitize.js +120 -45
- package/src/handler/plsql/procedureVariable.js +4 -2
- package/src/handler/plsql/request.js +8 -3
- package/src/handler/plsql/sendResponse.js +43 -5
- package/src/index.js +0 -1
- package/src/server/adminContext.js +23 -0
- package/src/server/config.js +1 -0
- package/src/server/server.js +130 -9
- package/src/types.js +7 -1
- package/src/util/cache.js +123 -0
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/statsManager.js +368 -0
- package/src/util/trace.js +7 -6
- package/src/util/traceManager.js +68 -0
- package/src/version.js +1 -1
- package/types/admin/js/schemas.d.ts +384 -0
- package/types/admin/js/types.d.ts +312 -0
- package/types/handler/handlerAdmin.d.ts +79 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/owaPageStream.d.ts +28 -0
- package/types/handler/plsql/procedure.d.ts +4 -1
- package/types/handler/plsql/procedureNamed.d.ts +2 -5
- package/types/handler/plsql/procedureSanitize.d.ts +2 -5
- package/types/handler/plsql/procedureVariable.d.ts +1 -1
- package/types/handler/plsql/request.d.ts +3 -1
- package/types/handler/plsql/sendResponse.d.ts +1 -1
- package/types/index.d.ts +0 -1
- package/types/server/adminContext.d.ts +17 -0
- package/types/server/server.d.ts +5 -0
- package/types/types.d.ts +19 -1
- package/types/util/cache.d.ts +69 -0
- package/types/util/jsonLogger.d.ts +45 -0
- package/types/util/statsManager.d.ts +395 -0
- package/types/util/traceManager.d.ts +35 -0
- package/src/handler/handlerMetrics.js +0 -68
- 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
|
+
});
|
package/src/handler/plsql/cgi.js
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
26
|
+
* @returns {messageType} - The output.
|
|
25
27
|
*/
|
|
26
|
-
const
|
|
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
|
|
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
|
|
74
|
-
const
|
|
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}
|
|
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,
|
|
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 {
|
|
66
|
+
* @returns {WebPlSqlRequestHandler} - The handler.
|
|
57
67
|
*/
|
|
58
68
|
export const handlerWebPlSql = (connectionPool, config) => {
|
|
59
69
|
debug('options', config);
|
|
60
70
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
};
|