badmfck-api-server 4.1.58 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apiServer/APIService.d.ts +1 -0
- package/dist/apiServer/APIService.js +2 -1
- package/dist/apiServer/MonitorService.d.ts +23 -1
- package/dist/apiServer/MonitorService.js +278 -2
- package/dist/apiServer/helper/Validator.js +14 -0
- package/dist/apiServer/monitor/Monitor.d.ts +2 -0
- package/dist/apiServer/monitor/Monitor.js +12 -0
- package/dist/apiServer/monitor/index.html +4 -4
- package/package.json +14 -14
|
@@ -51,6 +51,7 @@ export interface APIServiceOptions<TInterceptor = unknown> {
|
|
|
51
51
|
appVersion?: string;
|
|
52
52
|
fileTempDir: string;
|
|
53
53
|
fileLimit: number;
|
|
54
|
+
metricsDir?: string;
|
|
54
55
|
}
|
|
55
56
|
type InferInterceptor<T> = T extends IInterceptor<infer U> ? U : unknown;
|
|
56
57
|
export declare const getDefaultOptions: <I extends IInterceptor<any> | undefined>(overrides?: Omit<Partial<APIServiceOptions<InferInterceptor<NonNullable<I>>>>, "interceptor"> & {
|
|
@@ -79,6 +79,7 @@ function defaultOptions() {
|
|
|
79
79
|
documentation: []
|
|
80
80
|
},
|
|
81
81
|
appVersion: undefined,
|
|
82
|
+
metricsDir: undefined,
|
|
82
83
|
fileTempDir: "/tmp",
|
|
83
84
|
fileLimit: 10_000_000,
|
|
84
85
|
keepAliveTimeout: 25 * 1000,
|
|
@@ -165,7 +166,7 @@ class APIService extends BaseService_1.BaseService {
|
|
|
165
166
|
new Monitor_1.Monitor(this.options),
|
|
166
167
|
];
|
|
167
168
|
}
|
|
168
|
-
this.monitor = new MonitorService_1.MonitorService();
|
|
169
|
+
this.monitor = new MonitorService_1.MonitorService(this.options.metricsDir);
|
|
169
170
|
this.monitor.init();
|
|
170
171
|
new DocumentService_1.DocumentGenerator(this.options.endpoints);
|
|
171
172
|
this.options.endpoints.push(new Liveness_1.Liveness(this.started), new Readiness_1.Readiness(this.started));
|
|
@@ -22,6 +22,10 @@ export declare const S_STAT_REGISTRATE_SQL_REQUEST: Signal<{
|
|
|
22
22
|
export declare const REQ_STAT: Req<{
|
|
23
23
|
range: "week" | "day" | "hour";
|
|
24
24
|
}, IStat[]>;
|
|
25
|
+
export declare const REQ_STAT_WEEKS: Req<void, string[]>;
|
|
26
|
+
export declare const REQ_STAT_WEEK: Req<{
|
|
27
|
+
weekKey: string;
|
|
28
|
+
}, IStat[]>;
|
|
25
29
|
type IMonitorSlot = {
|
|
26
30
|
yyyymmddhhii: number;
|
|
27
31
|
cpu: number;
|
|
@@ -43,8 +47,14 @@ export declare class MonitorService extends BaseService {
|
|
|
43
47
|
intervalMs: number;
|
|
44
48
|
lastCpuUsage: NodeJS.CpuUsage | null;
|
|
45
49
|
lastTimestamp: number | null;
|
|
46
|
-
|
|
50
|
+
private metricsDir;
|
|
51
|
+
private static MAX_WEEKS;
|
|
52
|
+
private _persistReady;
|
|
53
|
+
private _flushedUpTo;
|
|
54
|
+
private _lastSeenWeekKey;
|
|
55
|
+
constructor(metricsDir?: string | null);
|
|
47
56
|
init(): Promise<void>;
|
|
57
|
+
finishService(): Promise<void>;
|
|
48
58
|
getEPStat(req: {
|
|
49
59
|
range: "week" | "day" | "hour";
|
|
50
60
|
}): Promise<IStat[]>;
|
|
@@ -59,5 +69,17 @@ export declare class MonitorService extends BaseService {
|
|
|
59
69
|
private onHttpRequestRegistrate;
|
|
60
70
|
private onSQLRequestRegistrate;
|
|
61
71
|
private getSlot;
|
|
72
|
+
private liveFile;
|
|
73
|
+
private archiveFile;
|
|
74
|
+
private weekKey;
|
|
75
|
+
private atomicWrite;
|
|
76
|
+
private readJsonl;
|
|
77
|
+
private persistTick;
|
|
78
|
+
private rotate;
|
|
79
|
+
private downsampleWeek;
|
|
80
|
+
private mergeSlots;
|
|
81
|
+
private hydrate;
|
|
82
|
+
private listPersistedWeeks;
|
|
83
|
+
private readPersistedWeek;
|
|
62
84
|
}
|
|
63
85
|
export {};
|
|
@@ -36,32 +36,65 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.MonitorService = exports.REQ_STAT = exports.S_STAT_REGISTRATE_SQL_REQUEST = exports.S_STAT_REGISTRATE_HTTP_REQUEST = exports.S_STAT_REGISTRATE_SERVICE = exports.S_STAT_REGISTRATE_CUSTOM_EVENT = exports.S_STAT_REGISTRATE_REQUEST = void 0;
|
|
39
|
+
exports.MonitorService = exports.REQ_STAT_WEEK = exports.REQ_STAT_WEEKS = exports.REQ_STAT = exports.S_STAT_REGISTRATE_SQL_REQUEST = exports.S_STAT_REGISTRATE_HTTP_REQUEST = exports.S_STAT_REGISTRATE_SERVICE = exports.S_STAT_REGISTRATE_CUSTOM_EVENT = exports.S_STAT_REGISTRATE_REQUEST = void 0;
|
|
40
40
|
const badmfck_signal_1 = __importStar(require("badmfck-signal"));
|
|
41
41
|
const BaseService_1 = require("./BaseService");
|
|
42
42
|
const YYYYMMDDHH_1 = require("./helper/YYYYMMDDHH");
|
|
43
|
+
const LogService_1 = require("./LogService");
|
|
43
44
|
const os_1 = __importDefault(require("os"));
|
|
45
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
46
|
+
const path_1 = __importDefault(require("path"));
|
|
44
47
|
exports.S_STAT_REGISTRATE_REQUEST = new badmfck_signal_1.default();
|
|
45
48
|
exports.S_STAT_REGISTRATE_CUSTOM_EVENT = new badmfck_signal_1.default();
|
|
46
49
|
exports.S_STAT_REGISTRATE_SERVICE = new badmfck_signal_1.default();
|
|
47
50
|
exports.S_STAT_REGISTRATE_HTTP_REQUEST = new badmfck_signal_1.default();
|
|
48
51
|
exports.S_STAT_REGISTRATE_SQL_REQUEST = new badmfck_signal_1.default();
|
|
49
52
|
exports.REQ_STAT = new badmfck_signal_1.Req(undefined, "REQ_STAT");
|
|
53
|
+
exports.REQ_STAT_WEEKS = new badmfck_signal_1.Req(undefined, "REQ_STAT_WEEKS");
|
|
54
|
+
exports.REQ_STAT_WEEK = new badmfck_signal_1.Req(undefined, "REQ_STAT_WEEK");
|
|
50
55
|
class MonitorService extends BaseService_1.BaseService {
|
|
51
56
|
slots = new Map();
|
|
52
57
|
intervalMs = 59_000;
|
|
53
58
|
lastCpuUsage = null;
|
|
54
59
|
lastTimestamp = null;
|
|
55
|
-
|
|
60
|
+
metricsDir;
|
|
61
|
+
static MAX_WEEKS = 10;
|
|
62
|
+
_persistReady = false;
|
|
63
|
+
_flushedUpTo = 0;
|
|
64
|
+
_lastSeenWeekKey = null;
|
|
65
|
+
constructor(metricsDir) {
|
|
56
66
|
super("MonitorService");
|
|
67
|
+
this.metricsDir = metricsDir && metricsDir.length > 0 ? metricsDir : null;
|
|
57
68
|
setInterval(() => this.collectUsage(), this.intervalMs);
|
|
58
69
|
}
|
|
59
70
|
async init() {
|
|
60
71
|
super.init();
|
|
61
72
|
exports.REQ_STAT.listener = async (req) => this.getEPStat(req);
|
|
73
|
+
exports.REQ_STAT_WEEKS.listener = async () => this.listPersistedWeeks();
|
|
74
|
+
exports.REQ_STAT_WEEK.listener = async (req) => this.readPersistedWeek(req.weekKey);
|
|
62
75
|
exports.S_STAT_REGISTRATE_HTTP_REQUEST.subscribe((data) => this.onHttpRequestRegistrate(data));
|
|
63
76
|
exports.S_STAT_REGISTRATE_SQL_REQUEST.subscribe((data) => this.onSQLRequestRegistrate(data));
|
|
64
77
|
exports.S_STAT_REGISTRATE_REQUEST.subscribe((data) => this.onStatRegistrate(data));
|
|
78
|
+
if (this.metricsDir) {
|
|
79
|
+
try {
|
|
80
|
+
await promises_1.default.mkdir(this.metricsDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
(0, LogService_1.logWarn)("metrics: cannot create metricsDir, disk persistence disabled: " + (e?.message ?? e));
|
|
84
|
+
this.metricsDir = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (this.metricsDir) {
|
|
88
|
+
await this.hydrate();
|
|
89
|
+
await this.rotate();
|
|
90
|
+
this._lastSeenWeekKey = this.weekKey(new Date());
|
|
91
|
+
this._persistReady = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async finishService() {
|
|
95
|
+
await super.finishService();
|
|
96
|
+
if (this.metricsDir && this._persistReady)
|
|
97
|
+
await this.persistTick(true).catch(() => { });
|
|
65
98
|
}
|
|
66
99
|
async getEPStat(req) {
|
|
67
100
|
const allSlots = [];
|
|
@@ -98,6 +131,8 @@ class MonitorService extends BaseService_1.BaseService {
|
|
|
98
131
|
agg.cpu += s.cpu;
|
|
99
132
|
agg.memory.rss += s.memory.rss;
|
|
100
133
|
agg.memory.heapUsed += s.memory.heapUsed;
|
|
134
|
+
agg.memory.heapTotal += s.memory.heapTotal;
|
|
135
|
+
agg.memory.external += s.memory.external;
|
|
101
136
|
for (const [path, count] of Object.entries(s.successEp)) {
|
|
102
137
|
agg.successEp[path] = (agg.successEp[path] || 0) + count;
|
|
103
138
|
}
|
|
@@ -118,6 +153,8 @@ class MonitorService extends BaseService_1.BaseService {
|
|
|
118
153
|
agg.cpu = Number((agg.cpu / len).toFixed(2));
|
|
119
154
|
agg.memory.rss = Math.round(agg.memory.rss / len);
|
|
120
155
|
agg.memory.heapUsed = Math.round(agg.memory.heapUsed / len);
|
|
156
|
+
agg.memory.heapTotal = Math.round(agg.memory.heapTotal / len);
|
|
157
|
+
agg.memory.external = Math.round(agg.memory.external / len);
|
|
121
158
|
result.push({
|
|
122
159
|
timestamp: this.slotToTimestamp(agg.yyyymmddhhii),
|
|
123
160
|
slot: agg
|
|
@@ -160,6 +197,8 @@ class MonitorService extends BaseService_1.BaseService {
|
|
|
160
197
|
heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
|
|
161
198
|
external: Math.round(mem.external / 1024 / 1024),
|
|
162
199
|
};
|
|
200
|
+
if (this.metricsDir && this._persistReady)
|
|
201
|
+
await this.persistTick().catch(() => { });
|
|
163
202
|
}
|
|
164
203
|
static MAX_SLOT_KEYS = 100;
|
|
165
204
|
static normalizeQuery(query) {
|
|
@@ -240,5 +279,242 @@ class MonitorService extends BaseService_1.BaseService {
|
|
|
240
279
|
}
|
|
241
280
|
return slot;
|
|
242
281
|
}
|
|
282
|
+
liveFile(weekKey) {
|
|
283
|
+
return path_1.default.join(this.metricsDir, `metrics_${weekKey}.jsonl`);
|
|
284
|
+
}
|
|
285
|
+
archiveFile(weekKey) {
|
|
286
|
+
return path_1.default.join(this.metricsDir, `metrics_${weekKey}.json`);
|
|
287
|
+
}
|
|
288
|
+
weekKey(date) {
|
|
289
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
290
|
+
const dayNum = (d.getUTCDay() + 6) % 7;
|
|
291
|
+
d.setUTCDate(d.getUTCDate() - dayNum + 3);
|
|
292
|
+
const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4));
|
|
293
|
+
const firstDayNum = (firstThursday.getUTCDay() + 6) % 7;
|
|
294
|
+
firstThursday.setUTCDate(firstThursday.getUTCDate() - firstDayNum + 3);
|
|
295
|
+
const week = 1 + Math.round((d.getTime() - firstThursday.getTime()) / (7 * 24 * 3600 * 1000));
|
|
296
|
+
return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
297
|
+
}
|
|
298
|
+
async atomicWrite(file, content) {
|
|
299
|
+
const tmp = file + ".tmp";
|
|
300
|
+
await promises_1.default.writeFile(tmp, content, "utf-8");
|
|
301
|
+
await promises_1.default.rename(tmp, file);
|
|
302
|
+
}
|
|
303
|
+
async readJsonl(file) {
|
|
304
|
+
const txt = await promises_1.default.readFile(file, "utf-8");
|
|
305
|
+
const out = [];
|
|
306
|
+
for (const line of txt.split("\n")) {
|
|
307
|
+
const t = line.trim();
|
|
308
|
+
if (!t)
|
|
309
|
+
continue;
|
|
310
|
+
try {
|
|
311
|
+
out.push(JSON.parse(t));
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
async persistTick(includeCurrentMinute = false) {
|
|
318
|
+
if (!this.metricsDir)
|
|
319
|
+
return;
|
|
320
|
+
const nowKey = parseInt((0, YYYYMMDDHH_1.YYYYMMDDHH)(new Date(), "yyyymmddhhmm"));
|
|
321
|
+
const pending = [];
|
|
322
|
+
for (const dayMap of this.slots.values()) {
|
|
323
|
+
for (const slot of dayMap.values()) {
|
|
324
|
+
const k = slot.yyyymmddhhii;
|
|
325
|
+
if (k <= this._flushedUpTo)
|
|
326
|
+
continue;
|
|
327
|
+
if (!includeCurrentMinute && k >= nowKey)
|
|
328
|
+
continue;
|
|
329
|
+
pending.push(slot);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
pending.sort((a, b) => a.yyyymmddhhii - b.yyyymmddhhii);
|
|
333
|
+
if (pending.length) {
|
|
334
|
+
try {
|
|
335
|
+
const byWeek = new Map();
|
|
336
|
+
for (const s of pending) {
|
|
337
|
+
const wk = this.weekKey(new Date(this.slotToTimestamp(s.yyyymmddhhii)));
|
|
338
|
+
byWeek.set(wk, (byWeek.get(wk) ?? "") + JSON.stringify(s) + "\n");
|
|
339
|
+
}
|
|
340
|
+
for (const [wk, text] of byWeek)
|
|
341
|
+
await promises_1.default.appendFile(this.liveFile(wk), text, "utf-8");
|
|
342
|
+
this._flushedUpTo = pending[pending.length - 1].yyyymmddhhii;
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
(0, LogService_1.logWarn)("metrics: persist append failed: " + (e?.message ?? e));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const curWeek = this.weekKey(new Date());
|
|
349
|
+
if (this._lastSeenWeekKey && this._lastSeenWeekKey !== curWeek)
|
|
350
|
+
await this.rotate().catch(() => { });
|
|
351
|
+
this._lastSeenWeekKey = curWeek;
|
|
352
|
+
}
|
|
353
|
+
async rotate() {
|
|
354
|
+
if (!this.metricsDir)
|
|
355
|
+
return;
|
|
356
|
+
try {
|
|
357
|
+
const curWeek = this.weekKey(new Date());
|
|
358
|
+
const files = await promises_1.default.readdir(this.metricsDir);
|
|
359
|
+
for (const f of files) {
|
|
360
|
+
const m = f.match(/^metrics_(\d{4}-W\d{2})\.jsonl$/);
|
|
361
|
+
if (m && m[1] !== curWeek)
|
|
362
|
+
await this.downsampleWeek(m[1]);
|
|
363
|
+
}
|
|
364
|
+
const after = await promises_1.default.readdir(this.metricsDir);
|
|
365
|
+
const byWeek = new Map();
|
|
366
|
+
for (const f of after) {
|
|
367
|
+
const m = f.match(/^metrics_(\d{4}-W\d{2})\.(jsonl|json)$/);
|
|
368
|
+
if (!m)
|
|
369
|
+
continue;
|
|
370
|
+
const arr = byWeek.get(m[1]) ?? [];
|
|
371
|
+
arr.push(f);
|
|
372
|
+
byWeek.set(m[1], arr);
|
|
373
|
+
}
|
|
374
|
+
const weeks = Array.from(byWeek.keys()).sort();
|
|
375
|
+
const excess = weeks.slice(0, Math.max(0, weeks.length - MonitorService.MAX_WEEKS));
|
|
376
|
+
for (const wk of excess)
|
|
377
|
+
for (const f of byWeek.get(wk))
|
|
378
|
+
await promises_1.default.unlink(path_1.default.join(this.metricsDir, f)).catch(() => { });
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
(0, LogService_1.logWarn)("metrics: rotate failed: " + (e?.message ?? e));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async downsampleWeek(weekKey) {
|
|
385
|
+
const live = this.liveFile(weekKey);
|
|
386
|
+
let slots;
|
|
387
|
+
try {
|
|
388
|
+
slots = await this.readJsonl(live);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!slots.length) {
|
|
394
|
+
await promises_1.default.unlink(live).catch(() => { });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const byHour = new Map();
|
|
398
|
+
for (const s of slots) {
|
|
399
|
+
const hourKey = Math.floor(s.yyyymmddhhii / 100) * 100;
|
|
400
|
+
const arr = byHour.get(hourKey) ?? [];
|
|
401
|
+
arr.push(s);
|
|
402
|
+
byHour.set(hourKey, arr);
|
|
403
|
+
}
|
|
404
|
+
const hourly = Array.from(byHour.entries())
|
|
405
|
+
.sort((a, b) => a[0] - b[0])
|
|
406
|
+
.map(([hourKey, arr]) => this.mergeSlots(hourKey, arr));
|
|
407
|
+
await this.atomicWrite(this.archiveFile(weekKey), JSON.stringify({ version: 1, weekKey, resolution: "hour", slots: hourly }));
|
|
408
|
+
await promises_1.default.unlink(live).catch(() => { });
|
|
409
|
+
}
|
|
410
|
+
mergeSlots(key, arr) {
|
|
411
|
+
const out = {
|
|
412
|
+
yyyymmddhhii: key, cpu: 0,
|
|
413
|
+
memory: { rss: 0, heapUsed: 0, heapTotal: 0, external: 0 },
|
|
414
|
+
successEp: {}, failEp: {}, http: {}, success_sql: {}, fail_sql: {},
|
|
415
|
+
};
|
|
416
|
+
const add = (dst, src) => {
|
|
417
|
+
for (const [k, v] of Object.entries(src))
|
|
418
|
+
dst[k] = (dst[k] || 0) + v;
|
|
419
|
+
};
|
|
420
|
+
for (const s of arr) {
|
|
421
|
+
out.cpu += s.cpu;
|
|
422
|
+
out.memory.rss += s.memory.rss;
|
|
423
|
+
out.memory.heapUsed += s.memory.heapUsed;
|
|
424
|
+
out.memory.heapTotal += s.memory.heapTotal;
|
|
425
|
+
out.memory.external += s.memory.external;
|
|
426
|
+
add(out.successEp, s.successEp);
|
|
427
|
+
add(out.failEp, s.failEp);
|
|
428
|
+
add(out.http, s.http);
|
|
429
|
+
add(out.success_sql, s.success_sql);
|
|
430
|
+
add(out.fail_sql, s.fail_sql);
|
|
431
|
+
}
|
|
432
|
+
const n = arr.length || 1;
|
|
433
|
+
out.cpu = Number((out.cpu / n).toFixed(2));
|
|
434
|
+
out.memory.rss = Math.round(out.memory.rss / n);
|
|
435
|
+
out.memory.heapUsed = Math.round(out.memory.heapUsed / n);
|
|
436
|
+
out.memory.heapTotal = Math.round(out.memory.heapTotal / n);
|
|
437
|
+
out.memory.external = Math.round(out.memory.external / n);
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
async hydrate() {
|
|
441
|
+
if (!this.metricsDir)
|
|
442
|
+
return;
|
|
443
|
+
try {
|
|
444
|
+
const cutoff = parseInt((0, YYYYMMDDHH_1.YYYYMMDDHH)(new Date(Date.now() - 7 * 24 * 3600 * 1000), "yyyymmddhhmm"));
|
|
445
|
+
const weeks = new Set([
|
|
446
|
+
this.weekKey(new Date()),
|
|
447
|
+
this.weekKey(new Date(Date.now() - 7 * 24 * 3600 * 1000)),
|
|
448
|
+
]);
|
|
449
|
+
let maxKey = 0;
|
|
450
|
+
for (const wk of weeks) {
|
|
451
|
+
let slots;
|
|
452
|
+
try {
|
|
453
|
+
slots = await this.readJsonl(this.liveFile(wk));
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
for (const s of slots) {
|
|
459
|
+
if (s.yyyymmddhhii < cutoff)
|
|
460
|
+
continue;
|
|
461
|
+
const dayKey = Math.floor(s.yyyymmddhhii / 100);
|
|
462
|
+
let dayMap = this.slots.get(dayKey);
|
|
463
|
+
if (!dayMap) {
|
|
464
|
+
dayMap = new Map();
|
|
465
|
+
this.slots.set(dayKey, dayMap);
|
|
466
|
+
}
|
|
467
|
+
dayMap.set(s.yyyymmddhhii, s);
|
|
468
|
+
if (s.yyyymmddhhii > maxKey)
|
|
469
|
+
maxKey = s.yyyymmddhhii;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this._flushedUpTo = maxKey;
|
|
473
|
+
while (this.slots.size > 7) {
|
|
474
|
+
const oldest = Array.from(this.slots.keys()).sort((a, b) => a - b)[0];
|
|
475
|
+
this.slots.delete(oldest);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
(0, LogService_1.logWarn)("metrics: hydrate failed: " + (e?.message ?? e));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async listPersistedWeeks() {
|
|
483
|
+
if (!this.metricsDir)
|
|
484
|
+
return [];
|
|
485
|
+
try {
|
|
486
|
+
const files = await promises_1.default.readdir(this.metricsDir);
|
|
487
|
+
const weeks = new Set();
|
|
488
|
+
for (const f of files) {
|
|
489
|
+
const m = f.match(/^metrics_(\d{4}-W\d{2})\.(jsonl|json)$/);
|
|
490
|
+
if (m)
|
|
491
|
+
weeks.add(m[1]);
|
|
492
|
+
}
|
|
493
|
+
return Array.from(weeks).sort();
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async readPersistedWeek(weekKey) {
|
|
500
|
+
if (!this.metricsDir || !/^\d{4}-W\d{2}$/.test(weekKey))
|
|
501
|
+
return [];
|
|
502
|
+
let slots = [];
|
|
503
|
+
try {
|
|
504
|
+
try {
|
|
505
|
+
slots = await this.readJsonl(this.liveFile(weekKey));
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
const txt = await promises_1.default.readFile(this.archiveFile(weekKey), "utf-8");
|
|
509
|
+
slots = (JSON.parse(txt)?.slots ?? []);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
return slots
|
|
516
|
+
.sort((a, b) => a.yyyymmddhhii - b.yyyymmddhhii)
|
|
517
|
+
.map(s => ({ timestamp: this.slotToTimestamp(s.yyyymmddhhii), slot: s }));
|
|
518
|
+
}
|
|
243
519
|
}
|
|
244
520
|
exports.MonitorService = MonitorService;
|
|
@@ -401,6 +401,20 @@ class Validator {
|
|
|
401
401
|
else if (Array.isArray(tmpl) && Array.isArray(item)) {
|
|
402
402
|
out[i] = this.syncStructure(tmpl, item);
|
|
403
403
|
}
|
|
404
|
+
else if (!this.isPlainObject(tmpl) && !Array.isArray(tmpl)) {
|
|
405
|
+
const wantType = typeof tmpl;
|
|
406
|
+
if (typeof item === wantType) {
|
|
407
|
+
out[i] = item;
|
|
408
|
+
}
|
|
409
|
+
else if (wantType === "string" || wantType === "number" || wantType === "boolean") {
|
|
410
|
+
const converted = this.convertToType(item, wantType);
|
|
411
|
+
const bad = converted === null || (wantType === "number" && Number.isNaN(converted));
|
|
412
|
+
out[i] = bad ? this.clone(tmpl) : converted;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
out[i] = item;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
404
418
|
else {
|
|
405
419
|
out[i] = this.syncStructure(tmpl, this.clone(tmpl));
|
|
406
420
|
}
|
|
@@ -34,6 +34,8 @@ export declare class Monitor extends BaseEndpoint {
|
|
|
34
34
|
httpstats(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
35
35
|
inventory(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
36
36
|
metrics(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
37
|
+
metricsWeeks(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
38
|
+
metricsWeek(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
37
39
|
connections(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
38
40
|
db(req: HTTPRequestVO): Promise<TransferPacketVO>;
|
|
39
41
|
checkAuthentication(req: HTTPRequestVO): Promise<string>;
|
|
@@ -56,6 +56,10 @@ class Monitor extends BaseEndpoint_1.BaseEndpoint {
|
|
|
56
56
|
range: "",
|
|
57
57
|
$__range_values: ["week", "day", "hour"]
|
|
58
58
|
} },
|
|
59
|
+
{ ignoreInterceptor: true, endpoint: "metricsWeeks", handler: this.metricsWeeks },
|
|
60
|
+
{ ignoreInterceptor: true, endpoint: "metricsWeek", handler: this.metricsWeek, validationModel: {
|
|
61
|
+
weekKey: ""
|
|
62
|
+
} },
|
|
59
63
|
{ ignoreInterceptor: true, endpoint: "connections", handler: this.connections },
|
|
60
64
|
{ ignoreInterceptor: true, endpoint: "db", handler: this.db, validationModel: {
|
|
61
65
|
range: "",
|
|
@@ -201,6 +205,14 @@ class Monitor extends BaseEndpoint_1.BaseEndpoint {
|
|
|
201
205
|
const result = await MonitorService_1.REQ_STAT.request(req.data);
|
|
202
206
|
return { data: result };
|
|
203
207
|
}
|
|
208
|
+
async metricsWeeks(req) {
|
|
209
|
+
await this.checkAuthentication(req);
|
|
210
|
+
return { data: await MonitorService_1.REQ_STAT_WEEKS.request() };
|
|
211
|
+
}
|
|
212
|
+
async metricsWeek(req) {
|
|
213
|
+
await this.checkAuthentication(req);
|
|
214
|
+
return { data: await MonitorService_1.REQ_STAT_WEEK.request({ weekKey: req.data.weekKey }) };
|
|
215
|
+
}
|
|
204
216
|
async connections(req) {
|
|
205
217
|
await this.checkAuthentication(req);
|
|
206
218
|
return { data: Http_1.Http.getActiveConnectionsSnapshot() };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<!-- Build: 2026-
|
|
5
|
-
<script>var __ver = "1.0.
|
|
4
|
+
<!-- Build: 2026-09-09T16:39:44.398Z | Version: 1.0.8 -->
|
|
5
|
+
<script>var __ver = "1.0.8";</script>
|
|
6
6
|
<meta charset="UTF-8" />
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
8
8
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -127,7 +127,7 @@ Error generating stack: `+i.message+`
|
|
|
127
127
|
&div:nth-child(3) {
|
|
128
128
|
animation-delay: -0.15s;
|
|
129
129
|
}
|
|
130
|
-
`,sm=({color:e,size:t=40})=>a.jsxs(M5,{$size:t,$color:e||"#665374",children:[a.jsx("div",{}),a.jsx("div",{}),a.jsx("div",{}),a.jsx("div",{})]}),qt=({children:e,onClose:t,label:r,scrollable:n,decorator:o,busy:i,sx:s,contentPadding:l})=>{o==="absolute"&&n&&(e=a.jsx(W,{sx:{display:o==="absolute"?"flex":void 0,flexDirection:o==="absolute"?"column":void 0,maxHeight:o==="absolute"?"80vh":void 0,zIndex:o==="absolute"?100:void 0,overflow:"auto"},children:e}));const c=r?a.jsx(W,{sx:{padding:l??"30px",boxSizing:"border-box",overflowX:"hidden",overflowY:"auto",flexGrow:1},children:e}):e,u=a.jsxs(W,{sx:{border:"1px solid rgba(255, 255, 255, 0.05)",padding:r?void 0:"30px",borderRadius:"6px",boxShadow:"0px 0px 100px rgba(0,0,0,.1)",pointerEvents:i?"none":"auto",userSelect:i?"none":"auto",display:"flex",position:"relative",flexDirection:"column",backgroundColor:"panelBackgroundPrimary",...s},children:[i&&a.jsxs(W,{sx:{position:"absolute",top:"0px",left:"0px",width:"100%",height:"100%",backgroundColor:"rgba(255,255,255,.5)",zIndex:1e5,display:"flex",flexDirection:"column",gap:"10px",justifyContent:"center",alignItems:"center"},children:[a.jsx(sm,{color:"#ad1bf1"}),a.jsx(O5,{children:"Please wait..."})]}),r&&a.jsxs(a.Fragment,{children:[a.jsx(W,{sx:{backgroundColor:o==="absolute"?"panelHeadBackgroundColor":void 0,padding:"20px",boxSizing:"border-box",width:"100%"},children:a.jsx(M,{variant:"head",sx:{display:"flex",paddingRight:"30px",overflowWrap:"break-word",wordBreak:"break-all",width:"100%",fontWeight:o==="absolute"?"200":"500",fontSize:o==="absolute"?"1.4rem":"1.2rem"},children:r})}),a.jsx(W,{sx:{boxSizing:"border-box",background:"panelDividerBackgroundColor",height:"2px",maxHeight:"2px"}})]}),t&&a.jsx(im,{icon:a.jsx(I5,{}),sx:{position:"absolute",top:"20px",right:"20px"},onClick:t}),c,i&&a.jsx(W,{sx:{position:"absolute",top:"0px",left:"0px",width:"100%",height:"100%",backdropFilter:"saturate(0.2)",zIndex:100001}})]});let d=a.jsx(a.Fragment,{children:u});return o==="absolute"&&(d=a.jsx(W,{sx:{position:"fixed",top:"0",left:"0",width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",zIndex:10,backgroundColor:"rgba(0,0,0,.6)"},children:u})),d},H5="#9a96ad",P=({children:e,sx:t})=>a.jsx(M,{variant:"caption",sx:{opacity:1,color:H5,letterSpacing:".02em",...t},children:e}),U5=new tt.Req(e=>m4(e)),W5=new tt.Req(e=>F1(e)),q5=new tt.Req(()=>g4()),V5=new tt.Req(e=>A4(e)),Q5=new tt.Req(()=>v4()),G5=new tt.Req(e=>y4(e)),K5=new tt.Req(()=>C4()),X5=new tt.Req(()=>E4()),am=new tt.Req(()=>w4()),Y5=new tt.Req(()=>B4()),J5=new tt.Req(()=>k4()),Z5=new tt.Req(()=>S4()),e8=new tt.Req(e=>b4(e.confirm)),Pt=(e,t,r=!0)=>{const n=V.useRef(e);n.current=e,V.useEffect(()=>{if(!r)return;n.current();const o=setInterval(()=>n.current(),t);return()=>clearInterval(o)},[t,r])},gi=ge.input`
|
|
130
|
+
`,sm=({color:e,size:t=40})=>a.jsxs(M5,{$size:t,$color:e||"#665374",children:[a.jsx("div",{}),a.jsx("div",{}),a.jsx("div",{}),a.jsx("div",{})]}),qt=({children:e,onClose:t,label:r,scrollable:n,decorator:o,busy:i,sx:s,contentPadding:l})=>{o==="absolute"&&n&&(e=a.jsx(W,{sx:{display:o==="absolute"?"flex":void 0,flexDirection:o==="absolute"?"column":void 0,maxHeight:o==="absolute"?"80vh":void 0,zIndex:o==="absolute"?100:void 0,overflow:"auto"},children:e}));const c=r?a.jsx(W,{sx:{padding:l??"30px",boxSizing:"border-box",overflowX:"hidden",overflowY:"auto",flexGrow:1},children:e}):e,u=a.jsxs(W,{sx:{border:"1px solid rgba(255, 255, 255, 0.05)",padding:r?void 0:"30px",borderRadius:"6px",boxShadow:"0px 0px 100px rgba(0,0,0,.1)",pointerEvents:i?"none":"auto",userSelect:i?"none":"auto",display:"flex",position:"relative",flexDirection:"column",backgroundColor:"panelBackgroundPrimary",...s},children:[i&&a.jsxs(W,{sx:{position:"absolute",top:"0px",left:"0px",width:"100%",height:"100%",backgroundColor:"rgba(255,255,255,.5)",zIndex:1e5,display:"flex",flexDirection:"column",gap:"10px",justifyContent:"center",alignItems:"center"},children:[a.jsx(sm,{color:"#ad1bf1"}),a.jsx(O5,{children:"Please wait..."})]}),r&&a.jsxs(a.Fragment,{children:[a.jsx(W,{sx:{backgroundColor:o==="absolute"?"panelHeadBackgroundColor":void 0,padding:"20px",boxSizing:"border-box",width:"100%"},children:a.jsx(M,{variant:"head",sx:{display:"flex",paddingRight:"30px",overflowWrap:"break-word",wordBreak:"break-all",width:"100%",fontWeight:o==="absolute"?"200":"500",fontSize:o==="absolute"?"1.4rem":"1.2rem"},children:r})}),a.jsx(W,{sx:{boxSizing:"border-box",background:"panelDividerBackgroundColor",height:"2px",maxHeight:"2px"}})]}),t&&a.jsx(im,{icon:a.jsx(I5,{}),sx:{position:"absolute",top:"20px",right:"20px"},onClick:t}),c,i&&a.jsx(W,{sx:{position:"absolute",top:"0px",left:"0px",width:"100%",height:"100%",backdropFilter:"saturate(0.2)",zIndex:100001}})]});let d=a.jsx(a.Fragment,{children:u});return o==="absolute"&&(d=a.jsx(W,{sx:{position:"fixed",top:"0",left:"0",width:"100%",height:"100%",display:"flex",justifyContent:"center",alignItems:"center",zIndex:10,backgroundColor:"rgba(0,0,0,.6)"},children:u})),d},H5="#9a96ad",P=({children:e,sx:t})=>a.jsx(M,{variant:"caption",sx:{opacity:1,color:H5,letterSpacing:".02em",...t},children:e}),U5=new tt.Req(e=>m4(e)),W5=new tt.Req(e=>F1(e)),q5=new tt.Req(()=>g4()),V5=new tt.Req(e=>A4(e)),Q5=new tt.Req(()=>v4()),G5=new tt.Req(e=>y4(e)),K5=new tt.Req(()=>C4()),X5=new tt.Req(()=>E4()),am=new tt.Req(()=>w4()),Y5=new tt.Req(()=>B4()),J5=new tt.Req(()=>k4()),Z5=new tt.Req(()=>S4()),e8=new tt.Req(e=>e!=null&&e.confirm?b4(e.confirm):Promise.resolve({httpStatus:0})),Pt=(e,t,r=!0)=>{const n=V.useRef(e);n.current=e,V.useEffect(()=>{if(!r)return;n.current();const o=setInterval(()=>n.current(),t);return()=>clearInterval(o)},[t,r])},gi=ge.input`
|
|
131
131
|
width: 100%;
|
|
132
132
|
box-sizing: border-box;
|
|
133
133
|
padding: 13px 14px;
|
|
@@ -447,4 +447,4 @@ const j6={background:"#a7a7a7",backgroundSelected:"#66bb6a",toggle:"#FFFFFF"},D6
|
|
|
447
447
|
<body>
|
|
448
448
|
<div id="root"></div>
|
|
449
449
|
</body>
|
|
450
|
-
</html>
|
|
450
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badmfck-api-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Simple API http server based on express",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,23 +20,23 @@
|
|
|
20
20
|
"express"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@types/express-fileupload": "^1.5.
|
|
24
|
-
"@types/mysql": "^2.15.
|
|
25
|
-
"@types/node": "^
|
|
26
|
-
"@types/pg": "^8.
|
|
27
|
-
"@types/ws": "^8.
|
|
23
|
+
"@types/express-fileupload": "^1.5.1",
|
|
24
|
+
"@types/mysql": "^2.15.27",
|
|
25
|
+
"@types/node": "^26.5.0",
|
|
26
|
+
"@types/pg": "^8.23.1",
|
|
27
|
+
"@types/ws": "^8.18.1",
|
|
28
28
|
"badmfck-signal": "^1.4.9",
|
|
29
|
-
"cors": "^2.8.
|
|
30
|
-
"express": "^
|
|
29
|
+
"cors": "^2.8.6",
|
|
30
|
+
"express": "^5.2.1",
|
|
31
31
|
"express-fileupload": "^1.5.2",
|
|
32
|
-
"mysql2": "^3.
|
|
33
|
-
"pg": "^8.
|
|
34
|
-
"undici": "^7.
|
|
35
|
-
"ws": "^8.
|
|
32
|
+
"mysql2": "^3.24.4",
|
|
33
|
+
"pg": "^8.23.0",
|
|
34
|
+
"undici": "^7.29.1",
|
|
35
|
+
"ws": "^8.21.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/cors": "^2.8.
|
|
39
|
-
"@types/express": "^
|
|
38
|
+
"@types/cors": "^2.8.19",
|
|
39
|
+
"@types/express": "^5.0.6"
|
|
40
40
|
},
|
|
41
41
|
"author": "Igor Bloom",
|
|
42
42
|
"license": "MIT"
|