web_plsql 1.0.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 +5 -5
- package/package.json +5 -5
- package/src/admin/client/charts.ts +303 -4
- package/src/admin/index.html +283 -69
- package/src/admin/js/api.ts +95 -20
- package/src/admin/js/app.ts +460 -152
- package/src/admin/js/schemas.ts +76 -14
- package/src/admin/js/templates/config.ts +10 -9
- package/src/admin/js/templates/errorRow.ts +8 -5
- package/src/admin/js/templates/index.ts +1 -0
- package/src/admin/js/templates/poolCard.ts +6 -6
- package/src/admin/js/templates/traceRow.ts +24 -0
- package/src/admin/js/types.ts +132 -19
- package/src/admin/js/ui/components.ts +44 -0
- package/src/admin/js/ui/table.ts +173 -0
- package/src/admin/js/ui/views.ts +311 -48
- package/src/admin/js/util/format.ts +22 -3
- 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 +54 -47
- package/src/admin/style.css +143 -27
- package/src/handler/handlerAdmin.js +121 -26
- package/src/handler/plsql/errorPage.js +0 -4
- package/src/handler/plsql/owaPageStream.js +93 -0
- package/src/handler/plsql/procedure.js +191 -121
- package/src/handler/plsql/procedureNamed.js +17 -3
- package/src/handler/plsql/procedureSanitize.js +24 -0
- package/src/handler/plsql/procedureVariable.js +2 -0
- package/src/handler/plsql/sendResponse.js +41 -3
- package/src/index.js +0 -1
- package/src/server/adminContext.js +23 -0
- package/src/server/server.js +83 -68
- package/src/types.js +1 -1
- package/src/util/statsManager.js +368 -0
- package/src/util/trace.js +2 -1
- 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 +70 -0
- package/types/handler/plsql/owaPageStream.d.ts +28 -0
- package/types/handler/plsql/procedure.d.ts +1 -0
- package/types/index.d.ts +0 -1
- package/types/server/adminContext.d.ts +17 -0
- package/types/server/server.d.ts +2 -19
- package/types/types.d.ts +1 -1
- package/types/util/statsManager.d.ts +395 -0
- package/types/util/traceManager.d.ts +35 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
- package/src/handler/handlerMetrics.js +0 -68
- package/types/handler/handlerMetrics.d.ts +0 -25
package/src/admin/style.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
/* Material Design 3 inspired tokens - Light Mode */
|
|
3
|
+
--accent-rgb: 59, 130, 246;
|
|
3
4
|
--bg-main: #f8fafc;
|
|
4
5
|
--bg-side: #fff;
|
|
5
6
|
--bg-card: #fff;
|
|
@@ -427,6 +428,50 @@ tr:last-child td {
|
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
/* System View Compact */
|
|
432
|
+
.view-system-compact {
|
|
433
|
+
padding: 20px 32px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.view-system-compact .metrics-grid {
|
|
437
|
+
display: grid;
|
|
438
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
439
|
+
gap: 16px;
|
|
440
|
+
align-items: start;
|
|
441
|
+
margin-bottom: 24px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.view-system-compact .card {
|
|
445
|
+
padding: 16px;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.view-system-compact .stat-row {
|
|
449
|
+
padding: 4px 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.view-system-compact .card-header {
|
|
453
|
+
margin-bottom: 12px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.resource-card {
|
|
457
|
+
display: flex;
|
|
458
|
+
flex-direction: column;
|
|
459
|
+
min-height: 350px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.resource-card .chart-container-mini {
|
|
463
|
+
margin-top: auto;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.chart-container-mini {
|
|
467
|
+
height: 100px;
|
|
468
|
+
width: 100%;
|
|
469
|
+
position: relative;
|
|
470
|
+
overflow: hidden;
|
|
471
|
+
border: none;
|
|
472
|
+
border-radius: 4px;
|
|
473
|
+
}
|
|
474
|
+
|
|
430
475
|
/* Theme Toggle */
|
|
431
476
|
|
|
432
477
|
/* Chart Styles */
|
|
@@ -1055,6 +1100,13 @@ nav button .material-symbols-rounded {
|
|
|
1055
1100
|
text-align: left;
|
|
1056
1101
|
}
|
|
1057
1102
|
|
|
1103
|
+
/* Dense table variant */
|
|
1104
|
+
.dense-table th,
|
|
1105
|
+
.dense-table td {
|
|
1106
|
+
padding: 6px 12px;
|
|
1107
|
+
font-size: 0.8125rem;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1058
1110
|
/* Code style */
|
|
1059
1111
|
.code {
|
|
1060
1112
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
@@ -1138,45 +1190,95 @@ nav button .material-symbols-rounded {
|
|
|
1138
1190
|
font-size: 28px;
|
|
1139
1191
|
}
|
|
1140
1192
|
|
|
1141
|
-
/*
|
|
1142
|
-
.
|
|
1193
|
+
/* Modal */
|
|
1194
|
+
.modal {
|
|
1143
1195
|
position: fixed;
|
|
1144
1196
|
inset: 0;
|
|
1145
|
-
background-color: rgba(0, 0, 0, 0.
|
|
1197
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
1146
1198
|
display: flex;
|
|
1147
1199
|
align-items: center;
|
|
1148
1200
|
justify-content: center;
|
|
1149
|
-
z-index:
|
|
1201
|
+
z-index: 1000;
|
|
1202
|
+
padding: 20px;
|
|
1150
1203
|
}
|
|
1151
1204
|
|
|
1152
|
-
.modal {
|
|
1205
|
+
.modal-content {
|
|
1153
1206
|
background-color: var(--bg-card);
|
|
1154
1207
|
border-radius: 12px;
|
|
1155
|
-
|
|
1156
|
-
max-width:
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1208
|
+
width: 100%;
|
|
1209
|
+
max-width: 900px;
|
|
1210
|
+
box-shadow: var(--shadow);
|
|
1211
|
+
display: flex;
|
|
1212
|
+
flex-direction: column;
|
|
1213
|
+
max-height: 90vh;
|
|
1161
1214
|
}
|
|
1162
1215
|
|
|
1163
|
-
.modal-
|
|
1216
|
+
.modal-header {
|
|
1217
|
+
padding: 16px 24px;
|
|
1218
|
+
border-bottom: 1px solid var(--border);
|
|
1219
|
+
display: flex;
|
|
1220
|
+
justify-content: space-between;
|
|
1221
|
+
align-items: center;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.modal-header h3 {
|
|
1225
|
+
margin: 0;
|
|
1164
1226
|
font-size: 1.125rem;
|
|
1165
|
-
font-weight: 600;
|
|
1166
1227
|
color: var(--text-bright);
|
|
1167
|
-
margin-bottom: 8px;
|
|
1168
1228
|
}
|
|
1169
1229
|
|
|
1170
|
-
.modal-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
margin-bottom: 24px;
|
|
1230
|
+
.modal-body {
|
|
1231
|
+
padding: 24px;
|
|
1232
|
+
overflow-y: auto;
|
|
1174
1233
|
}
|
|
1175
1234
|
|
|
1176
|
-
.
|
|
1235
|
+
.stat-values-inline {
|
|
1177
1236
|
display: flex;
|
|
1178
|
-
|
|
1179
|
-
|
|
1237
|
+
align-items: center;
|
|
1238
|
+
gap: 8px;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/* Stat values for min/max */
|
|
1242
|
+
.stat-values {
|
|
1243
|
+
display: flex;
|
|
1244
|
+
flex-direction: column;
|
|
1245
|
+
align-items: flex-end;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.min-max {
|
|
1249
|
+
font-size: 0.7rem;
|
|
1250
|
+
opacity: 0.7;
|
|
1251
|
+
margin-top: 2px;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/* Search Group */
|
|
1255
|
+
.search-group {
|
|
1256
|
+
position: relative;
|
|
1257
|
+
display: flex;
|
|
1258
|
+
align-items: center;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.input-sm {
|
|
1262
|
+
background-color: var(--bg-side);
|
|
1263
|
+
border: 1px solid var(--border);
|
|
1264
|
+
border-radius: 6px;
|
|
1265
|
+
color: var(--text-bright);
|
|
1266
|
+
padding: 6px 12px;
|
|
1267
|
+
font-size: 0.875rem;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.input-sm:focus {
|
|
1271
|
+
border-color: var(--accent);
|
|
1272
|
+
outline: none;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.errors-table-row {
|
|
1276
|
+
cursor: pointer;
|
|
1277
|
+
transition: background-color 0.2s;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.errors-table-row:hover {
|
|
1281
|
+
background-color: rgba(var(--accent-rgb), 0.08);
|
|
1180
1282
|
}
|
|
1181
1283
|
|
|
1182
1284
|
/* Server status indicator enhanced */
|
|
@@ -1238,12 +1340,6 @@ nav button .material-symbols-rounded {
|
|
|
1238
1340
|
margin-bottom: 8px;
|
|
1239
1341
|
}
|
|
1240
1342
|
|
|
1241
|
-
.danger-zone-text {
|
|
1242
|
-
color: var(--text-main);
|
|
1243
|
-
font-size: 0.875rem;
|
|
1244
|
-
margin-bottom: 16px;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
1343
|
/* Empty state */
|
|
1248
1344
|
.empty-state {
|
|
1249
1345
|
text-align: center;
|
|
@@ -1319,3 +1415,23 @@ select {
|
|
|
1319
1415
|
accent-color: var(--accent);
|
|
1320
1416
|
cursor: pointer;
|
|
1321
1417
|
}
|
|
1418
|
+
|
|
1419
|
+
/* Animations */
|
|
1420
|
+
@keyframes spin {
|
|
1421
|
+
from {
|
|
1422
|
+
transform: rotate(0deg);
|
|
1423
|
+
}
|
|
1424
|
+
to {
|
|
1425
|
+
transform: rotate(360deg);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.animate-spin {
|
|
1430
|
+
animation: spin 1s linear infinite;
|
|
1431
|
+
display: inline-block;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
.btn.loading {
|
|
1435
|
+
pointer-events: none;
|
|
1436
|
+
opacity: 0.8;
|
|
1437
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import readline from 'node:readline';
|
|
4
|
-
import {AdminContext} from '../server/
|
|
5
|
+
import {AdminContext} from '../server/adminContext.js';
|
|
6
|
+
import {traceManager} from '../util/traceManager.js';
|
|
5
7
|
import {getVersion} from '../version.js';
|
|
6
8
|
|
|
7
9
|
const version = getVersion();
|
|
@@ -13,15 +15,40 @@ import {forceShutdown} from '../util/shutdown.js';
|
|
|
13
15
|
* @typedef {import('express').NextFunction} NextFunction
|
|
14
16
|
*/
|
|
15
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
|
+
|
|
16
42
|
export const handlerAdmin = express.Router();
|
|
17
43
|
|
|
18
44
|
/**
|
|
19
45
|
* Helper to read last N lines of a file
|
|
20
46
|
* @param {string} filePath - Path to file
|
|
21
47
|
* @param {number} n - Number of lines
|
|
48
|
+
* @param {string} [filter] - Optional filter string
|
|
22
49
|
* @returns {Promise<string[]>} - The lines
|
|
23
50
|
*/
|
|
24
|
-
const readLastLines = async (filePath, n = 100) => {
|
|
51
|
+
const readLastLines = async (filePath, n = 100, filter = '') => {
|
|
25
52
|
if (!fs.existsSync(filePath)) {
|
|
26
53
|
return [];
|
|
27
54
|
}
|
|
@@ -32,12 +59,16 @@ const readLastLines = async (filePath, n = 100) => {
|
|
|
32
59
|
crlfDelay: Infinity,
|
|
33
60
|
});
|
|
34
61
|
|
|
62
|
+
const filterLower = filter.toLowerCase();
|
|
63
|
+
|
|
35
64
|
/** @type {string[]} */
|
|
36
65
|
const lines = [];
|
|
37
66
|
for await (const line of rl) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
lines.
|
|
67
|
+
if (!filter || line.toLowerCase().includes(filterLower)) {
|
|
68
|
+
lines.push(line);
|
|
69
|
+
if (lines.length > n) {
|
|
70
|
+
lines.shift();
|
|
71
|
+
}
|
|
41
72
|
}
|
|
42
73
|
}
|
|
43
74
|
return lines;
|
|
@@ -48,42 +79,72 @@ handlerAdmin.get('/api/status', (_req, res) => {
|
|
|
48
79
|
const uptime = (new Date().getTime() - AdminContext.startTime.getTime()) / 1000;
|
|
49
80
|
|
|
50
81
|
const poolStats = AdminContext.pools.map((pool, index) => {
|
|
51
|
-
const
|
|
82
|
+
const cache = AdminContext.caches[index];
|
|
83
|
+
const name = cache?.poolName ?? `pool-${index}`;
|
|
52
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
|
+
|
|
53
88
|
return {
|
|
54
89
|
name,
|
|
55
90
|
stats: typeof p.getStatistics === 'function' ? p.getStatistics() : null,
|
|
56
91
|
connectionsOpen: pool.connectionsOpen,
|
|
57
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
|
+
},
|
|
58
105
|
};
|
|
59
106
|
});
|
|
60
107
|
|
|
61
108
|
const memUsage = process.memoryUsage();
|
|
109
|
+
const systemMemoryUsed = os.totalmem() - os.freemem();
|
|
62
110
|
const cpuUsage = process.cpuUsage();
|
|
111
|
+
const summary = /** @type {StatsSummary} */ (AdminContext.statsManager.getSummary());
|
|
63
112
|
|
|
64
113
|
res.json({
|
|
65
114
|
version,
|
|
66
115
|
status: AdminContext.paused ? 'paused' : 'running',
|
|
67
116
|
uptime,
|
|
68
117
|
startTime: AdminContext.startTime,
|
|
118
|
+
intervalMs: AdminContext.statsManager.config.intervalMs,
|
|
69
119
|
metrics: {
|
|
70
|
-
|
|
71
|
-
|
|
120
|
+
requestCount: summary.totalRequests,
|
|
121
|
+
errorCount: summary.totalErrors,
|
|
122
|
+
avgResponseTime: summary.avgResponseTime,
|
|
123
|
+
minResponseTime: summary.minResponseTime,
|
|
124
|
+
maxResponseTime: summary.maxResponseTime,
|
|
125
|
+
maxRequestsPerSecond: summary.maxRequestsPerSecond,
|
|
72
126
|
},
|
|
127
|
+
history: AdminContext.statsManager.getHistory(),
|
|
73
128
|
pools: poolStats,
|
|
74
129
|
system: {
|
|
75
130
|
nodeVersion: process.version,
|
|
76
131
|
platform: process.platform,
|
|
77
132
|
arch: process.arch,
|
|
133
|
+
cpuCores: os.cpus().length,
|
|
78
134
|
memory: {
|
|
79
|
-
rss:
|
|
135
|
+
rss: systemMemoryUsed,
|
|
80
136
|
heapTotal: memUsage.heapTotal,
|
|
81
137
|
heapUsed: memUsage.heapUsed,
|
|
82
138
|
external: memUsage.external,
|
|
139
|
+
totalMemory: os.totalmem(),
|
|
140
|
+
...summary.maxMemory,
|
|
83
141
|
},
|
|
84
142
|
cpu: {
|
|
85
143
|
user: cpuUsage.user,
|
|
86
144
|
system: cpuUsage.system,
|
|
145
|
+
max: summary.cpu.max,
|
|
146
|
+
userMax: summary.cpu.userMax,
|
|
147
|
+
systemMax: summary.cpu.systemMax,
|
|
87
148
|
},
|
|
88
149
|
},
|
|
89
150
|
config: AdminContext.config
|
|
@@ -103,8 +164,9 @@ handlerAdmin.get('/api/status', (_req, res) => {
|
|
|
103
164
|
handlerAdmin.get('/api/logs/error', async (req, res) => {
|
|
104
165
|
try {
|
|
105
166
|
const limit = Number(req.query.limit) || 100;
|
|
167
|
+
const filter = typeof req.query.filter === 'string' ? req.query.filter : '';
|
|
106
168
|
const logFile = 'error.json.log';
|
|
107
|
-
const lines = await readLastLines(logFile, limit);
|
|
169
|
+
const lines = await readLastLines(logFile, limit, filter);
|
|
108
170
|
|
|
109
171
|
/** @type {Record<string, unknown>[]} */
|
|
110
172
|
const logs = [];
|
|
@@ -129,6 +191,7 @@ handlerAdmin.get('/api/logs/error', async (req, res) => {
|
|
|
129
191
|
handlerAdmin.get('/api/logs/access', async (req, res) => {
|
|
130
192
|
try {
|
|
131
193
|
const limit = Number(req.query.limit) || 100;
|
|
194
|
+
const filter = typeof req.query.filter === 'string' ? req.query.filter : '';
|
|
132
195
|
const logFile = AdminContext.config?.loggerFilename ?? 'access.log';
|
|
133
196
|
|
|
134
197
|
if (!AdminContext.config?.loggerFilename) {
|
|
@@ -136,28 +199,14 @@ handlerAdmin.get('/api/logs/access', async (req, res) => {
|
|
|
136
199
|
return;
|
|
137
200
|
}
|
|
138
201
|
|
|
139
|
-
const lines = await readLastLines(logFile, limit);
|
|
202
|
+
const lines = await readLastLines(logFile, limit, filter);
|
|
140
203
|
res.json(lines.reverse());
|
|
141
204
|
} catch (err) {
|
|
142
205
|
res.status(500).json({error: String(err)});
|
|
143
206
|
}
|
|
144
207
|
});
|
|
145
208
|
|
|
146
|
-
// GET /api/cache
|
|
147
|
-
handlerAdmin.get('/api/cache', (_req, res) => {
|
|
148
|
-
const caches = AdminContext.caches.map((c) => ({
|
|
149
|
-
poolName: c.poolName,
|
|
150
|
-
procedureNameCache: {
|
|
151
|
-
size: c.procedureNameCache.keys().length,
|
|
152
|
-
stats: c.procedureNameCache.getStats(),
|
|
153
|
-
},
|
|
154
|
-
argumentCache: {
|
|
155
|
-
size: c.argumentCache.keys().length,
|
|
156
|
-
stats: c.argumentCache.getStats(),
|
|
157
|
-
},
|
|
158
|
-
}));
|
|
159
|
-
res.json(caches);
|
|
160
|
-
});
|
|
209
|
+
// GET /api/cache - REMOVED (Merged into /api/status)
|
|
161
210
|
|
|
162
211
|
// POST /api/cache/clear
|
|
163
212
|
handlerAdmin.post('/api/cache/clear', (req, res) => {
|
|
@@ -196,3 +245,49 @@ handlerAdmin.post('/api/server/:action', (req, res) => {
|
|
|
196
245
|
res.status(400).json({error: 'Invalid action'});
|
|
197
246
|
}
|
|
198
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
|
+
});
|
|
@@ -8,7 +8,6 @@ import {getFormattedMessage, logToFile} from '../../util/trace.js';
|
|
|
8
8
|
import {errorToString} from '../../util/errorToString.js';
|
|
9
9
|
import {getHtmlPage} from '../../util/html.js';
|
|
10
10
|
import {jsonLogger} from '../../util/jsonLogger.js';
|
|
11
|
-
import {AdminContext} from '../../server/server.js';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* @typedef {import('express').Request} Request
|
|
@@ -76,9 +75,6 @@ export const errorPage = (req, res, options, error) => {
|
|
|
76
75
|
// get error data
|
|
77
76
|
const errorData = getErrorData(req, error);
|
|
78
77
|
|
|
79
|
-
// Update metrics
|
|
80
|
-
AdminContext.metrics.errorCount++;
|
|
81
|
-
|
|
82
78
|
// get formatted message
|
|
83
79
|
const {html, text} = getFormattedMessage(errorData);
|
|
84
80
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {Readable} from 'node:stream';
|
|
2
|
+
import oracledb from 'oracledb';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
import debugModule from 'debug';
|
|
5
|
+
import {ProcedureError} from './procedureError.js';
|
|
6
|
+
import {errorToString} from '../../util/errorToString.js';
|
|
7
|
+
|
|
8
|
+
const debug = debugModule('webplsql:owaPageStream');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
12
|
+
* @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class OWAPageStream extends Readable {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Connection} databaseConnection - The database connection.
|
|
18
|
+
*/
|
|
19
|
+
constructor(databaseConnection) {
|
|
20
|
+
super({highWaterMark: 16384}); // 16KB buffer
|
|
21
|
+
this.databaseConnection = databaseConnection;
|
|
22
|
+
this.chunkSize = 1000;
|
|
23
|
+
this.isDone = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch a chunk of the page from the database.
|
|
28
|
+
* @returns {Promise<string[]>} The array of lines fetched.
|
|
29
|
+
*/
|
|
30
|
+
async fetchChunk() {
|
|
31
|
+
if (this.isDone) return [];
|
|
32
|
+
|
|
33
|
+
/** @type {BindParameterConfig} */
|
|
34
|
+
const bindParameter = {
|
|
35
|
+
lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxArraySize: this.chunkSize},
|
|
36
|
+
irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: this.chunkSize},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const sqlStatement = 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;';
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = await this.databaseConnection.execute(sqlStatement, bindParameter);
|
|
43
|
+
const {lines, irows} = z.object({irows: z.number(), lines: z.array(z.string())}).parse(result.outBinds);
|
|
44
|
+
|
|
45
|
+
debug(`fetched ${lines.length} lines (irows=${irows})`);
|
|
46
|
+
|
|
47
|
+
// If we got fewer lines than requested, OR if we got 0 lines, we are done
|
|
48
|
+
if (lines.length < this.chunkSize) {
|
|
49
|
+
this.isDone = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err instanceof ProcedureError) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
throw new ProcedureError(`OWAPageStream: error when getting page\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @override
|
|
63
|
+
* @param {number} _size - The size hint (unused).
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
_read(_size) {
|
|
67
|
+
this.fetchChunk()
|
|
68
|
+
.then((lines) => {
|
|
69
|
+
if (lines.length > 0) {
|
|
70
|
+
this.push(lines.join(''));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// After fetching, check if we're done
|
|
74
|
+
if (this.isDone) {
|
|
75
|
+
this.push(null);
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.catch((/** @type {unknown} */ err) => {
|
|
79
|
+
this.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add initial body content to the stream.
|
|
85
|
+
* @param {string} content - The initial content to prepend.
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
addBody(content) {
|
|
89
|
+
if (content && content.length > 0) {
|
|
90
|
+
this.push(content);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|