@futdevpro/nts-dynamo 1.15.119 → 1.15.121
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/.dynamo/logs/cicd-pipeline/output.log +1899 -1878
- package/.dynamo/logs/cicd-pipeline/status.json +37 -37
- package/build/_models/control-models/endpoint-params.control-model.d.ts +5 -0
- package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
- package/build/_models/control-models/endpoint-params.control-model.js +41 -11
- package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts +6 -1
- package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js +40 -10
- package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts +9 -0
- package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js +12 -0
- package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts +12 -2
- package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js +54 -7
- package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js.map +1 -1
- package/build/_modules/server/server-status/server-status.control-service.d.ts +6 -0
- package/build/_modules/server/server-status/server-status.control-service.d.ts.map +1 -1
- package/build/_modules/server/server-status/server-status.control-service.js +10 -0
- package/build/_modules/server/server-status/server-status.control-service.js.map +1 -1
- package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
- package/build/_modules/server/server-status/server-status.controller.js +26 -0
- package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
- package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts +11 -1
- package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts.map +1 -1
- package/build/_modules/socket/_models/socket-server-service-params.control-model.js +12 -1
- package/build/_modules/socket/_models/socket-server-service-params.control-model.js.map +1 -1
- package/build/_modules/socket/_services/socket-server.service.d.ts +2 -0
- package/build/_modules/socket/_services/socket-server.service.d.ts.map +1 -1
- package/build/_modules/socket/_services/socket-server.service.js +2 -0
- package/build/_modules/socket/_services/socket-server.service.js.map +1 -1
- package/build/_modules/socket/app-extended.server.d.ts.map +1 -1
- package/build/_modules/socket/app-extended.server.js +13 -8
- package/build/_modules/socket/app-extended.server.js.map +1 -1
- package/build/_services/base/db.service.d.ts +14 -0
- package/build/_services/base/db.service.d.ts.map +1 -1
- package/build/_services/base/db.service.js +27 -0
- package/build/_services/base/db.service.js.map +1 -1
- package/build/_services/core/api.service.d.ts.map +1 -1
- package/build/_services/core/api.service.js +29 -9
- package/build/_services/core/api.service.js.map +1 -1
- package/build/_services/core/db-stats.service.d.ts +34 -0
- package/build/_services/core/db-stats.service.d.ts.map +1 -0
- package/build/_services/core/db-stats.service.js +65 -0
- package/build/_services/core/db-stats.service.js.map +1 -0
- package/build/_services/core/diagnostics.service.d.ts +43 -0
- package/build/_services/core/diagnostics.service.d.ts.map +1 -0
- package/build/_services/core/diagnostics.service.js +122 -0
- package/build/_services/core/diagnostics.service.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/_models/control-models/endpoint-params.control-model.ts +44 -16
- package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.spec.ts +16 -0
- package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.ts +50 -14
- package/src/_modules/local-vector-search/_services/lvs-vector-persist.data-service.ts +22 -0
- package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.spec.ts +5 -3
- package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.ts +62 -11
- package/src/_modules/server/server-status/server-status.control-service.ts +12 -0
- package/src/_modules/server/server-status/server-status.controller.ts +28 -0
- package/src/_modules/socket/_models/socket-server-service-params.control-model.spec.ts +23 -0
- package/src/_modules/socket/_models/socket-server-service-params.control-model.ts +12 -1
- package/src/_modules/socket/_services/socket-server.service.ts +2 -0
- package/src/_modules/socket/app-extended.server.ts +15 -9
- package/src/_services/base/db.service.ts +39 -1
- package/src/_services/core/api.service.spec.ts +47 -0
- package/src/_services/core/api.service.ts +32 -12
- package/src/_services/core/db-stats.service.ts +104 -0
- package/src/_services/core/diagnostics.service.spec.ts +44 -0
- package/src/_services/core/diagnostics.service.ts +158 -0
- package/src/index.ts +2 -0
|
@@ -43,4 +43,27 @@ describe('| DyNTS_SocketServerService_Params', () => {
|
|
|
43
43
|
expect(instance.port).toBeUndefined();
|
|
44
44
|
expect(instance.security).toBe(DyNTS_SocketSecurity.open);
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
// BFR-MASTERPROMPTER-025 — az attach-mód EXPLICIT opt-in, a port önmagában NEM diszkriminátor
|
|
48
|
+
it('| dedicatedPort default FALSE (shared) — port-label önmagában nem vált dedikáltra', () => {
|
|
49
|
+
const instance = new DyNTS_SocketServerService_Params({
|
|
50
|
+
name: 'LegacyPortLabel',
|
|
51
|
+
port: 39047,
|
|
52
|
+
security: DyNTS_SocketSecurity.open,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(instance.dedicatedPort).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('| dedicatedPort: true explicit opt-in megőrződik', () => {
|
|
59
|
+
const instance = new DyNTS_SocketServerService_Params({
|
|
60
|
+
name: 'DedicatedNotif',
|
|
61
|
+
port: 39157,
|
|
62
|
+
dedicatedPort: true,
|
|
63
|
+
security: DyNTS_SocketSecurity.open,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(instance.dedicatedPort).toBe(true);
|
|
67
|
+
expect(instance.port).toBe(39157);
|
|
68
|
+
});
|
|
46
69
|
});
|
|
@@ -4,12 +4,22 @@ import { DyNTS_SocketSecurity } from '../_enums/socket-security.enum';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Socket service paraméterek.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* **BFR-MASTERPROMPTER-025 (attach-mód):** a socket alapból a fő HTTP/HTTPS szerverre attacholódik
|
|
9
|
+
* (SHARED — unified-port modell). Dedikált saját listener CSAK explicit `dedicatedPort: true`-val kérhető
|
|
10
|
+
* (+ `port` kötelező és ≠ fő port). A korábbi implicit inferencia (port≠httpPort → dedikált) csendben
|
|
11
|
+
* regresszálta azokat a unified-appokat, amelyek legacy-label okból adtak portot — ezért az attach-mód
|
|
12
|
+
* mostantól KÓD-EXPLICIT, a `port` önmagában NEM diszkriminátor.
|
|
8
13
|
*/
|
|
9
14
|
export class DyNTS_SocketServerService_Params<T = any> {
|
|
10
15
|
name: string;
|
|
11
16
|
/** Opcionális: ha nincs megadva, az extended app a HTTP/HTTPS portot használja (same port). */
|
|
12
17
|
port?: number;
|
|
18
|
+
/**
|
|
19
|
+
* EXPLICIT opt-in a dedikált saját listenerre (a fő szerver HELYETT). Default: false = shared.
|
|
20
|
+
* `true` esetén a `port` kötelező és el kell térnie a fő HTTP-porttól.
|
|
21
|
+
*/
|
|
22
|
+
dedicatedPort?: boolean;
|
|
13
23
|
security?: DyNTS_SocketSecurity;
|
|
14
24
|
|
|
15
25
|
constructor(
|
|
@@ -17,6 +27,7 @@ export class DyNTS_SocketServerService_Params<T = any> {
|
|
|
17
27
|
) {
|
|
18
28
|
this.name = set.name;
|
|
19
29
|
this.port = set.port;
|
|
30
|
+
this.dedicatedPort = set.dedicatedPort ?? false;
|
|
20
31
|
this.security = set.security ? set.security : DyNTS_global_settings.defaultSocketSecurity;
|
|
21
32
|
}
|
|
22
33
|
}
|
|
@@ -51,6 +51,8 @@ export abstract class DyNTS_SocketServerService<
|
|
|
51
51
|
protected params: DyNTS_SocketServerService_Params;
|
|
52
52
|
get name(): string { return this.params.name; }
|
|
53
53
|
get port(): number | undefined { return this.params.port; }
|
|
54
|
+
/** BFR-MASTERPROMPTER-025: explicit dedikált-listener opt-in (default false = shared). */
|
|
55
|
+
get dedicatedPort(): boolean { return this.params.dedicatedPort ?? false; }
|
|
54
56
|
get security(): DyNTS_SocketSecurity { return this.params.security; }
|
|
55
57
|
get activeSubscriptionsCount(): number { return this.presences.length; }
|
|
56
58
|
|
|
@@ -512,15 +512,21 @@ export abstract class DyNTS_AppExtended extends DyNTS_App {
|
|
|
512
512
|
if (service.security === DyNTS_SocketSecurity.open) {
|
|
513
513
|
this.systemControlsExt.httpSocketServer.init = true;
|
|
514
514
|
|
|
515
|
-
// BFR-
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
515
|
+
// BFR-MASTERPROMPTER-025 (a BFR-FDPTOKENSERVICE-007 3a2c381 utó-fix-e): az attach-mód
|
|
516
|
+
// EXPLICIT opt-in (`dedicatedPort: true`), NEM a port-érték implicit inferenciája. A
|
|
517
|
+
// korábbi `port≠httpPort → dedikált` inferencia CSENDBEN regresszálta a unified-shared
|
|
518
|
+
// appokat, amelyek legacy-label/notif-konvenció okból adtak portot (MP live-igazolt:
|
|
519
|
+
// realtime DEAD az nts-bump után; adventor/ccap/warbots latens). Mostantól:
|
|
520
|
+
// - default (dedicatedPort hiányzik/false) → SHARED (a fő szerverre attach, a port-label irreleváns);
|
|
521
|
+
// - dedicatedPort: true → saját Http.Server + listen a service.port-on (a bizonyított
|
|
522
|
+
// non-base útvonal tükre) — pl. a token-service szándékosan-dedikált notif-portja.
|
|
523
|
+
if (service.dedicatedPort && (service.port === undefined || service.port === this.portSettings.httpPort)) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`SOCKET CONFIG ERROR: "${service.name}" dedicatedPort:true requires an explicit ` +
|
|
526
|
+
`port that differs from the main httpPort (${this.portSettings.httpPort}).`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const attachToMain: boolean = useBaseHttpServer && !service.dedicatedPort;
|
|
524
530
|
const serverForOpen: Http.Server = attachToMain
|
|
525
531
|
? this.httpServer
|
|
526
532
|
: (useBaseHttpServer
|
|
@@ -677,8 +677,46 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
|
|
|
677
677
|
* @param narrowByDependencyIds id
|
|
678
678
|
* @returns dataList
|
|
679
679
|
*/
|
|
680
|
+
/**
|
|
681
|
+
* BFR-AM-013 — LEAN + projektált + BATCH-elt cursor-scan (OOM-safe nagy collection-ökhöz).
|
|
682
|
+
*
|
|
683
|
+
* A teljes result-set SOSEM él egyszerre a heapen: a Mongo-cursor batch-enként adagolja a
|
|
684
|
+
* PLAIN (lean, nem-Mongoose-doc) objektumokat a `handler`-nek — a Mongoose-doc-boxing tranziens
|
|
685
|
+
* (a 32k-vektoros LVS-hidratálás 93%-heap-spike-jának gyökere) kimarad. A `projection`-nel csak
|
|
686
|
+
* a szükséges mezők jönnek (pl. `{ vectorId: 1, embedding: 1 }` a content-szöveg NÉLKÜL).
|
|
687
|
+
* Visszaadja a feldolgozott dokumentumok számát.
|
|
688
|
+
*/
|
|
689
|
+
async findLeanBatched(
|
|
690
|
+
filterBy: DyFM_DBFilter<T>,
|
|
691
|
+
set: {
|
|
692
|
+
projection?: Record<string, number>,
|
|
693
|
+
batchSize?: number,
|
|
694
|
+
handler: (batch: Partial<T>[]) => Promise<void> | void,
|
|
695
|
+
}
|
|
696
|
+
): Promise<number> {
|
|
697
|
+
const batchSize: number = set.batchSize && set.batchSize > 0 ? set.batchSize : 500;
|
|
698
|
+
const cursor = this.dataModel.find(filterBy, set.projection).lean().cursor();
|
|
699
|
+
|
|
700
|
+
let batch: Partial<T>[] = [];
|
|
701
|
+
let total: number = 0;
|
|
702
|
+
|
|
703
|
+
for await (const doc of cursor) {
|
|
704
|
+
batch.push(doc as Partial<T>);
|
|
705
|
+
total++;
|
|
706
|
+
if (batch.length >= batchSize) {
|
|
707
|
+
await set.handler(batch);
|
|
708
|
+
batch = [];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (batch.length) {
|
|
712
|
+
await set.handler(batch);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return total;
|
|
716
|
+
}
|
|
717
|
+
|
|
680
718
|
async searchData(
|
|
681
|
-
filterBy: DyFM_DBFilter<T>,
|
|
719
|
+
filterBy: DyFM_DBFilter<T>,
|
|
682
720
|
narrowByDependencyIds?: string[],
|
|
683
721
|
narrowByDependencyKey?: string,
|
|
684
722
|
findDeleted?: boolean
|
|
@@ -138,3 +138,50 @@ describe('| DyNTS_ApiService', () => {
|
|
|
138
138
|
);
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
|
+
|
|
142
|
+
// BFR-DYNAMOBUILDER-006 — a belső BE-BE URL NEM kerülhet a _message-be (kliens-leak);
|
|
143
|
+
// a confidentialContent-be megy (a response-strip env-függetlenül levágja).
|
|
144
|
+
describe('| DyNTS_ApiService.handleError — internal URL leak guard (BFR-DYNAMOBUILDER-006)', () => {
|
|
145
|
+
const internalUrl: string = 'http://auth-service:39005/api/auth/login';
|
|
146
|
+
|
|
147
|
+
function buildCallParams(): DyNTS_ApiCall_Params {
|
|
148
|
+
return new DyNTS_ApiCall_Params({
|
|
149
|
+
baseUrl: 'http://auth-service:39005/api',
|
|
150
|
+
endpoint: '/auth/login',
|
|
151
|
+
type: DyFM_HttpCallType.post,
|
|
152
|
+
httpOptions: { headers: {}, responseType: DyFM_HttpResponseType.json },
|
|
153
|
+
name: 'loginCall',
|
|
154
|
+
getFullResponse: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function invokeHandleError(error: unknown): DyFM_Error {
|
|
159
|
+
try {
|
|
160
|
+
DyNTS_ApiService['handleError'](buildCallParams(), error, { pathParams: {}, body: {} }, internalUrl);
|
|
161
|
+
} catch (thrown: unknown) {
|
|
162
|
+
return thrown as DyFM_Error;
|
|
163
|
+
}
|
|
164
|
+
throw new Error('handleError did not throw');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
it('| HE3 (ECONNREFUSED): a _message NEM tartalmazza a belső URL-t; a confidentialContent igen', () => {
|
|
168
|
+
const thrown: DyFM_Error = invokeHandleError({ code: 'ECONNREFUSED', message: 'connect ECONNREFUSED' });
|
|
169
|
+
|
|
170
|
+
expect(thrown._message).not.toContain('auth-service:39005');
|
|
171
|
+
expect(thrown._message).toContain('loginCall');
|
|
172
|
+
expect(thrown.confidentialContent?.url).toBe(internalUrl);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('| HE4 (status code): a _message NEM tartalmazza a belső endpointot/URL-t', () => {
|
|
176
|
+
const axiosLike = {
|
|
177
|
+
config: {},
|
|
178
|
+
message: 'Request failed with status code 401',
|
|
179
|
+
response: { status: 401 },
|
|
180
|
+
};
|
|
181
|
+
const thrown: DyFM_Error = invokeHandleError(axiosLike);
|
|
182
|
+
|
|
183
|
+
expect(thrown._message).not.toContain('auth-service:39005');
|
|
184
|
+
expect(thrown.confidentialContent?.url).toBe(internalUrl);
|
|
185
|
+
expect(thrown.additionalContent?.callName).toBe('loginCall');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -264,12 +264,19 @@ export class DyNTS_ApiService {
|
|
|
264
264
|
),
|
|
265
265
|
|
|
266
266
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE1`,
|
|
267
|
-
|
|
267
|
+
// BFR-DYNAMOBUILDER-006: a BELSŐ BE-BE URL (Docker-hostname+port) NEM kerülhet a _message-be —
|
|
268
|
+
// non-prod env-en a teljes error kimegy a kliensnek → info-disclosure. A _message csak a
|
|
269
|
+
// hívás NEVÉT hordozza; az URL + callParams a confidentialContent-be megy (error-store-ba
|
|
270
|
+
// perzisztálódik, a response-strip env-FÜGGETLENÜL levágja).
|
|
271
|
+
message:
|
|
268
272
|
`API call "${callParams.name}" failed on the other end!`+
|
|
269
|
-
`\nurl: ${url}` +
|
|
270
273
|
`\nerror response found: ${errorMsg}`,
|
|
271
274
|
error: dyfmErrorInAxiosResponse,
|
|
272
275
|
additionalContent: {
|
|
276
|
+
callName: callParams.name,
|
|
277
|
+
},
|
|
278
|
+
confidentialContent: {
|
|
279
|
+
url: url,
|
|
273
280
|
callParams: callParams,
|
|
274
281
|
inputParams: inputParams,
|
|
275
282
|
response: (error as Axios.AxiosError)?.response,
|
|
@@ -287,8 +294,12 @@ export class DyNTS_ApiService {
|
|
|
287
294
|
|
|
288
295
|
status: 404,
|
|
289
296
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE2`,
|
|
290
|
-
message:
|
|
297
|
+
message: `DNS error; address cannot be resolved! (call: "${callParams.name}")`,
|
|
291
298
|
additionalContent: {
|
|
299
|
+
callName: callParams.name,
|
|
300
|
+
},
|
|
301
|
+
confidentialContent: {
|
|
302
|
+
url: url,
|
|
292
303
|
callParams: callParams,
|
|
293
304
|
inputParams: inputParams,
|
|
294
305
|
response: (error as Axios.AxiosError)?.response,
|
|
@@ -306,11 +317,15 @@ export class DyNTS_ApiService {
|
|
|
306
317
|
|
|
307
318
|
status: 404,
|
|
308
319
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE3`,
|
|
309
|
-
|
|
320
|
+
// BFR-DYNAMOBUILDER-006: az URL confidentialContent-be (nem a _message-be) — belső host-leak ellen.
|
|
321
|
+
message: `Can't connect to the endpoint! (call: "${callParams.name}")`,
|
|
310
322
|
additionalContent: {
|
|
323
|
+
callName: callParams.name,
|
|
324
|
+
},
|
|
325
|
+
confidentialContent: {
|
|
326
|
+
url: url,
|
|
311
327
|
callParams: callParams,
|
|
312
328
|
inputParams: inputParams,
|
|
313
|
-
url: url,
|
|
314
329
|
response: (error as Axios.AxiosError)?.response,
|
|
315
330
|
},
|
|
316
331
|
});
|
|
@@ -325,15 +340,14 @@ export class DyNTS_ApiService {
|
|
|
325
340
|
500;
|
|
326
341
|
|
|
327
342
|
if ((error as Axios.AxiosError)?.message?.includes('status code')) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
// BFR-DYNAMOBUILDER-006: a _message CSAK a hívás-nevet + path-param-KULCSOKAT hordozza —
|
|
344
|
+
// a belső endpoint/URL/stack a confidentialContent-be megy (response-ba SOHA).
|
|
345
|
+
msg += ` "${callParams.name}"` +
|
|
331
346
|
`\npathParams: ${
|
|
332
|
-
inputParams?.pathParams ?
|
|
333
|
-
Object.keys(inputParams.pathParams).join(', ') :
|
|
347
|
+
inputParams?.pathParams ?
|
|
348
|
+
Object.keys(inputParams.pathParams).join(', ') :
|
|
334
349
|
'-no path params-'
|
|
335
|
-
}
|
|
336
|
-
`\nenpointStackLocation: ${callParams.stack}`;
|
|
350
|
+
}`;
|
|
337
351
|
}
|
|
338
352
|
|
|
339
353
|
throw new DyFM_Error({
|
|
@@ -346,6 +360,12 @@ export class DyNTS_ApiService {
|
|
|
346
360
|
message: msg,
|
|
347
361
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE4`,
|
|
348
362
|
additionalContent: {
|
|
363
|
+
callName: callParams.name,
|
|
364
|
+
},
|
|
365
|
+
confidentialContent: {
|
|
366
|
+
url: url,
|
|
367
|
+
endpoint: `${callParams.type} "${callParams.baseUrl}${callParams.endpoint}"`,
|
|
368
|
+
endpointStackLocation: callParams.stack,
|
|
349
369
|
callParams: callParams,
|
|
350
370
|
inputParams: inputParams,
|
|
351
371
|
response: (error as Axios.AxiosError)?.response,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
import { DyFM_Log } from '@futdevpro/fsm-dynamo';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BFR-OVERSEER-004 — stats-only DB-diagnosztika (NO direct data read).
|
|
7
|
+
*
|
|
8
|
+
* Per-collection rekordszám + adat-méret + index-méret (a `$collStats` aggregation-stage-en át —
|
|
9
|
+
* metaadat-olvasás, SOHA nem dokumentum-tartalom) + összesített DB-méret. A 2026-06-23 overseer-incidens
|
|
10
|
+
* (3 GB-ra hízott archív-collection, csak kézi mongosh-sal látszott) proaktív early-warningja: minden
|
|
11
|
+
* NTS-szerver default megkapja a `/server/db-stats` endpointon (app-auth-tal gate-elhető).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface DyNTS_CollectionStats {
|
|
15
|
+
name: string;
|
|
16
|
+
documentCount: number;
|
|
17
|
+
sizeMB: number;
|
|
18
|
+
indexSizeMB: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DyNTS_DbStats {
|
|
22
|
+
dbName: string;
|
|
23
|
+
collections: DyNTS_CollectionStats[];
|
|
24
|
+
totalSizeMB: number;
|
|
25
|
+
totalIndexSizeMB: number;
|
|
26
|
+
totalDocumentCount: number;
|
|
27
|
+
/** A gyűjtés közben elhasalt collection-ök (never-silent — a kihagyás látható). */
|
|
28
|
+
failedCollections: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BYTES_PER_MB: number = 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
export class DyNTS_DbStats_Util {
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A teljes stats-only pillanatkép. Hibatűrő: egy-egy collection statjának elhasalása NEM
|
|
37
|
+
* borítja a többit (failedCollections-be kerül, warn-nal — no-silent-skip).
|
|
38
|
+
*/
|
|
39
|
+
static async collect(connection: mongoose.Connection): Promise<DyNTS_DbStats> {
|
|
40
|
+
const db: typeof connection.db = connection?.db;
|
|
41
|
+
if (!db) {
|
|
42
|
+
return {
|
|
43
|
+
dbName: connection?.name ?? '-no-connection-',
|
|
44
|
+
collections: [],
|
|
45
|
+
totalSizeMB: 0,
|
|
46
|
+
totalIndexSizeMB: 0,
|
|
47
|
+
totalDocumentCount: 0,
|
|
48
|
+
failedCollections: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const collectionInfos: { name: string }[] = await db.listCollections({}, { nameOnly: true }).toArray();
|
|
53
|
+
const collections: DyNTS_CollectionStats[] = [];
|
|
54
|
+
const failedCollections: string[] = [];
|
|
55
|
+
|
|
56
|
+
for (const info of collectionInfos) {
|
|
57
|
+
try {
|
|
58
|
+
// $collStats storageStats: metaadat-only (size/count/totalIndexSize) — nincs dokumentum-olvasás.
|
|
59
|
+
const statsCursor = db.collection(info.name).aggregate([
|
|
60
|
+
{ $collStats: { storageStats: {} } },
|
|
61
|
+
]);
|
|
62
|
+
const statsDocs: Record<string, unknown>[] = await statsCursor.toArray();
|
|
63
|
+
const storage: Record<string, unknown> =
|
|
64
|
+
(statsDocs[0]?.['storageStats'] ?? {}) as Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
collections.push({
|
|
67
|
+
name: info.name,
|
|
68
|
+
documentCount: Number(storage['count'] ?? 0),
|
|
69
|
+
sizeMB: DyNTS_DbStats_Util.toMB(Number(storage['size'] ?? 0)),
|
|
70
|
+
indexSizeMB: DyNTS_DbStats_Util.toMB(Number(storage['totalIndexSize'] ?? 0)),
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
failedCollections.push(info.name);
|
|
74
|
+
DyFM_Log.warn(`(DyNTS_DbStats) collStats failed for "${info.name}" (non-fatal)`, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Méret szerint csökkenő — a bloat-gyanús collection legyen elöl.
|
|
79
|
+
collections.sort((a: DyNTS_CollectionStats, b: DyNTS_CollectionStats): number => b.sizeMB - a.sizeMB);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
dbName: connection.name,
|
|
83
|
+
collections: collections,
|
|
84
|
+
totalSizeMB: DyNTS_DbStats_Util.round2(
|
|
85
|
+
collections.reduce((sum: number, c: DyNTS_CollectionStats): number => sum + c.sizeMB, 0)
|
|
86
|
+
),
|
|
87
|
+
totalIndexSizeMB: DyNTS_DbStats_Util.round2(
|
|
88
|
+
collections.reduce((sum: number, c: DyNTS_CollectionStats): number => sum + c.indexSizeMB, 0)
|
|
89
|
+
),
|
|
90
|
+
totalDocumentCount: collections.reduce(
|
|
91
|
+
(sum: number, c: DyNTS_CollectionStats): number => sum + c.documentCount, 0
|
|
92
|
+
),
|
|
93
|
+
failedCollections: failedCollections,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static toMB(bytes: number): number {
|
|
98
|
+
return DyNTS_DbStats_Util.round2(bytes / BYTES_PER_MB);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static round2(value: number): number {
|
|
102
|
+
return Math.round(value * 100) / 100;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DyNTS_DbStats_Util } from './db-stats.service';
|
|
2
|
+
import { DyNTS_Diagnostics, DyNTS_DoctorCheck } from './diagnostics.service';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BFR-AM-015 / BFR-OVERSEER-004 — a diagnosztika PURE részeinek unit-tesztjei
|
|
6
|
+
* (interpretHealth / buildSummary / MB-kerekítés).
|
|
7
|
+
*/
|
|
8
|
+
describe('| DyNTS_Diagnostics (pure)', () => {
|
|
9
|
+
|
|
10
|
+
const ok: DyNTS_DoctorCheck = { name: 'a', health: 'ok' };
|
|
11
|
+
const warn: DyNTS_DoctorCheck = { name: 'b', health: 'warn', fixHint: 'do b' };
|
|
12
|
+
const fail: DyNTS_DoctorCheck = { name: 'c', health: 'fail', fixHint: 'do c' };
|
|
13
|
+
|
|
14
|
+
it('| interpretHealth: fail dominál, aztán warn, különben ok', () => {
|
|
15
|
+
expect(DyNTS_Diagnostics.interpretHealth([ ok, ok ])).toBe('ok');
|
|
16
|
+
expect(DyNTS_Diagnostics.interpretHealth([ ok, warn ])).toBe('warn');
|
|
17
|
+
expect(DyNTS_Diagnostics.interpretHealth([ warn, fail, ok ])).toBe('fail');
|
|
18
|
+
expect(DyNTS_Diagnostics.interpretHealth([])).toBe('ok');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('| buildSummary: all-ok → "all checks ok"; nem-ok check-ek fixHint-tel listázva', () => {
|
|
22
|
+
expect(DyNTS_Diagnostics.buildSummary([ ok ])).toBe('all checks ok');
|
|
23
|
+
const summary: string = DyNTS_Diagnostics.buildSummary([ ok, warn, fail ]);
|
|
24
|
+
expect(summary).toContain('b: warn — do b');
|
|
25
|
+
expect(summary).toContain('c: fail — do c');
|
|
26
|
+
expect(summary).not.toContain('a:');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('| DyNTS_DbStats_Util (pure)', () => {
|
|
31
|
+
|
|
32
|
+
it('| toMB: byte → MB két tizedesre', () => {
|
|
33
|
+
expect(DyNTS_DbStats_Util.toMB(1024 * 1024)).toBe(1);
|
|
34
|
+
expect(DyNTS_DbStats_Util.toMB(1536 * 1024)).toBe(1.5);
|
|
35
|
+
expect(DyNTS_DbStats_Util.toMB(0)).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('| collect: kapcsolat nélkül üres, nem-dobó eredmény', async () => {
|
|
39
|
+
const stats = await DyNTS_DbStats_Util.collect(undefined as never);
|
|
40
|
+
expect(stats.collections).toEqual([]);
|
|
41
|
+
expect(stats.totalSizeMB).toBe(0);
|
|
42
|
+
expect(stats.failedCollections).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
import { DyFM_Log } from '@futdevpro/fsm-dynamo';
|
|
4
|
+
|
|
5
|
+
import { DyNTS_DbStats, DyNTS_DbStats_Util } from './db-stats.service';
|
|
6
|
+
import { DyNTS_EventLoopDiag } from './event-loop-diag.service';
|
|
7
|
+
import { DyNTS_MemoryGuard } from './memory-guard.service';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BFR-AM-015 (+ BFR-OVERSEER-004 aggregátor-fele) — generikus, újrahasználható diagnosztikai
|
|
11
|
+
* aggregátor MINDEN `DyNTS_App`-hoz: a `GET /server/doctor` endpoint forrása.
|
|
12
|
+
*
|
|
13
|
+
* Check-ek (mind SAJÁT try-ban — egy elhasaló check nem borítja a doctort, néma hiba TILOS):
|
|
14
|
+
* - mongo: kapcsolat + ping (readiness-minta);
|
|
15
|
+
* - memory: MemoryGuard pressure-snapshot;
|
|
16
|
+
* - eventLoop: EventLoopDiag snapshot;
|
|
17
|
+
* - dbStats: stats-only collection-méretek + küszöb-warn a bloat-gyanúra.
|
|
18
|
+
* Az app APP-SPECIFIKUS check-eket regisztrálhat (`registerCheck`) — pl. env-validáció,
|
|
19
|
+
* provider-probe (a FAM `FAM_Doctor` mintája bedrock-magként).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface DyNTS_DoctorCheck {
|
|
23
|
+
name: string;
|
|
24
|
+
health: 'ok' | 'warn' | 'fail';
|
|
25
|
+
detail?: unknown;
|
|
26
|
+
/** Mit tegyen az operátor, ha nem ok (néma hiba tilos — a hint kötelező nem-ok esetén). */
|
|
27
|
+
fixHint?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DyNTS_DoctorReport {
|
|
31
|
+
health: 'ok' | 'warn' | 'fail';
|
|
32
|
+
checks: DyNTS_DoctorCheck[];
|
|
33
|
+
summary: string;
|
|
34
|
+
uptimeSec: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** App-specifikus check-függvény (a doctor futtatja, saját try-ban). */
|
|
38
|
+
export type DyNTS_DoctorCheckFn = () => Promise<DyNTS_DoctorCheck>;
|
|
39
|
+
|
|
40
|
+
/** Collection-méret warn-küszöb (MB) — e fölött a dbStats-check warn-ra vált. */
|
|
41
|
+
const DEFAULT_COLLECTION_WARN_MB: number = 512;
|
|
42
|
+
|
|
43
|
+
export class DyNTS_Diagnostics {
|
|
44
|
+
|
|
45
|
+
private static readonly customChecks: DyNTS_DoctorCheckFn[] = [];
|
|
46
|
+
|
|
47
|
+
/** App-specifikus check regisztrálása (pl. env-validáció, provider-probe). */
|
|
48
|
+
static registerCheck(check: DyNTS_DoctorCheckFn): void {
|
|
49
|
+
DyNTS_Diagnostics.customChecks.push(check);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A teljes doctor-riport. SOSEM dob — a hibás check `fail`-ként jelenik meg. */
|
|
53
|
+
static async runDoctor(set: {
|
|
54
|
+
connection: mongoose.Connection,
|
|
55
|
+
collectionWarnMB?: number,
|
|
56
|
+
}): Promise<DyNTS_DoctorReport> {
|
|
57
|
+
const checks: DyNTS_DoctorCheck[] = [];
|
|
58
|
+
|
|
59
|
+
// 1) Mongo-probe (readiness-minta: readyState + admin ping).
|
|
60
|
+
try {
|
|
61
|
+
const readyState: number = set.connection?.readyState ?? 0;
|
|
62
|
+
let pingOk: boolean = false;
|
|
63
|
+
if (readyState === 1) {
|
|
64
|
+
await set.connection.db?.admin().ping();
|
|
65
|
+
pingOk = true;
|
|
66
|
+
}
|
|
67
|
+
checks.push({
|
|
68
|
+
name: 'mongo',
|
|
69
|
+
health: readyState === 1 && pingOk ? 'ok' : 'fail',
|
|
70
|
+
detail: { readyState: readyState, pingOk: pingOk },
|
|
71
|
+
fixHint: readyState === 1 && pingOk ? undefined
|
|
72
|
+
: 'Check MONGO_URL + DB container/network reachability (readyState 1 + ping expected).',
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
checks.push({
|
|
76
|
+
name: 'mongo', health: 'fail', detail: String(error),
|
|
77
|
+
fixHint: 'Mongo ping threw — check DB availability/credentials.',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2) Memória (MemoryGuard pressure).
|
|
82
|
+
try {
|
|
83
|
+
const pressure: unknown = DyNTS_MemoryGuard.getInstance().pressure();
|
|
84
|
+
const level: string = String((pressure as { level?: string })?.level ?? 'ok');
|
|
85
|
+
checks.push({
|
|
86
|
+
name: 'memory',
|
|
87
|
+
health: level === 'critical' ? 'fail' : level === 'warning' ? 'warn' : 'ok',
|
|
88
|
+
detail: pressure,
|
|
89
|
+
fixHint: level === 'ok' ? undefined
|
|
90
|
+
: 'Heap pressure — check collection sizes (/server/db-stats) + recent load; consider retention.',
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
checks.push({ name: 'memory', health: 'warn', detail: String(error), fixHint: 'MemoryGuard snapshot failed.' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 3) Event-loop lag.
|
|
97
|
+
try {
|
|
98
|
+
const snapshot: unknown = DyNTS_EventLoopDiag.getInstance().getSnapshot();
|
|
99
|
+
checks.push({ name: 'eventLoop', health: 'ok', detail: snapshot });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
checks.push({ name: 'eventLoop', health: 'warn', detail: String(error), fixHint: 'EventLoopDiag snapshot failed.' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4) DB-stats + bloat-küszöb (BFR-OVERSEER-004 early-warning).
|
|
105
|
+
try {
|
|
106
|
+
const stats: DyNTS_DbStats = await DyNTS_DbStats_Util.collect(set.connection);
|
|
107
|
+
const warnMB: number = set.collectionWarnMB ?? DEFAULT_COLLECTION_WARN_MB;
|
|
108
|
+
const bloated: string[] = stats.collections
|
|
109
|
+
.filter((c): boolean => c.sizeMB + c.indexSizeMB > warnMB)
|
|
110
|
+
.map((c): string => `${c.name} (${c.sizeMB}MB + ${c.indexSizeMB}MB idx)`);
|
|
111
|
+
checks.push({
|
|
112
|
+
name: 'dbStats',
|
|
113
|
+
health: bloated.length ? 'warn' : 'ok',
|
|
114
|
+
detail: { totalSizeMB: stats.totalSizeMB, collectionCount: stats.collections.length, bloated: bloated },
|
|
115
|
+
fixHint: bloated.length
|
|
116
|
+
? `Collections over ${warnMB}MB — add dataParams.retention (TTL) or prune: ${bloated.join(', ')}`
|
|
117
|
+
: undefined,
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
checks.push({ name: 'dbStats', health: 'warn', detail: String(error), fixHint: 'db-stats collection failed.' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5) App-specifikus check-ek.
|
|
124
|
+
for (const customCheck of DyNTS_Diagnostics.customChecks) {
|
|
125
|
+
try {
|
|
126
|
+
checks.push(await customCheck());
|
|
127
|
+
} catch (error) {
|
|
128
|
+
DyFM_Log.warn('(DyNTS_Diagnostics) custom check threw (reported as fail)', error);
|
|
129
|
+
checks.push({ name: 'custom', health: 'fail', detail: String(error), fixHint: 'Custom check threw — see detail.' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
health: DyNTS_Diagnostics.interpretHealth(checks),
|
|
135
|
+
checks: checks,
|
|
136
|
+
summary: DyNTS_Diagnostics.buildSummary(checks),
|
|
137
|
+
uptimeSec: Math.round(process.uptime()),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Aggregált health: bármely fail → fail; különben bármely warn → warn; különben ok. Pure (unit-tesztelt). */
|
|
142
|
+
static interpretHealth(checks: DyNTS_DoctorCheck[]): 'ok' | 'warn' | 'fail' {
|
|
143
|
+
if (checks.some((c: DyNTS_DoctorCheck): boolean => c.health === 'fail')) { return 'fail'; }
|
|
144
|
+
if (checks.some((c: DyNTS_DoctorCheck): boolean => c.health === 'warn')) { return 'warn'; }
|
|
145
|
+
return 'ok';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Ember-olvasható összegzés a nem-ok check-ekből. Pure (unit-tesztelt). */
|
|
149
|
+
static buildSummary(checks: DyNTS_DoctorCheck[]): string {
|
|
150
|
+
const notOk: DyNTS_DoctorCheck[] = checks.filter((c: DyNTS_DoctorCheck): boolean => c.health !== 'ok');
|
|
151
|
+
if (!notOk.length) {
|
|
152
|
+
return 'all checks ok';
|
|
153
|
+
}
|
|
154
|
+
return notOk
|
|
155
|
+
.map((c: DyNTS_DoctorCheck): string => `${c.name}: ${c.health}${c.fixHint ? ` — ${c.fixHint}` : ''}`)
|
|
156
|
+
.join(' | ');
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export * from './_collections/security-headers.util';
|
|
|
58
58
|
export * from './_collections/client-safe-error.util';
|
|
59
59
|
export * from './_collections/load-shed.util';
|
|
60
60
|
export * from './_services/core/event-loop-diag.service';
|
|
61
|
+
export * from './_services/core/db-stats.service';
|
|
62
|
+
export * from './_services/core/diagnostics.service';
|
|
61
63
|
|
|
62
64
|
// models/CONTROL MODELS
|
|
63
65
|
export * from './_models/control-models/api-call-params.control-model';
|