@futdevpro/nts-dynamo 1.15.118 → 1.15.120
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 +1845 -1703
- package/.dynamo/logs/cicd-pipeline/status.json +42 -36
- 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/_models/data-models/schema-migration.data-model.d.ts +29 -0
- package/build/_models/data-models/schema-migration.data-model.d.ts.map +1 -0
- package/build/_models/data-models/schema-migration.data-model.js +47 -0
- package/build/_models/data-models/schema-migration.data-model.js.map +1 -0
- package/build/_models/interfaces/migration-entry.interface.d.ts +20 -0
- package/build/_models/interfaces/migration-entry.interface.d.ts.map +1 -0
- package/build/_models/interfaces/migration-entry.interface.js +3 -0
- package/build/_models/interfaces/migration-entry.interface.js.map +1 -0
- 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/_services/core/migration-runner.service.d.ts +40 -0
- package/build/_services/core/migration-runner.service.d.ts.map +1 -0
- package/build/_services/core/migration-runner.service.js +75 -0
- package/build/_services/core/migration-runner.service.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +6 -0
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/_models/control-models/endpoint-params.control-model.ts +44 -16
- package/src/_models/data-models/schema-migration.data-model.ts +50 -0
- package/src/_models/interfaces/migration-entry.interface.ts +19 -0
- 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/_services/core/migration-runner.service.spec.ts +91 -0
- package/src/_services/core/migration-runner.service.ts +106 -0
- package/src/index.ts +7 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DyNTS_Migration_Runner } from './migration-runner.service';
|
|
2
|
+
import { DyNTS_SchemaMigration } from '../../_models/data-models/schema-migration.data-model';
|
|
3
|
+
import { DyNTS_MigrationEntry } from '../../_models/interfaces/migration-entry.interface';
|
|
4
|
+
|
|
5
|
+
function entry(id: string, run: () => Promise<{ touched: number }>): DyNTS_MigrationEntry {
|
|
6
|
+
return { id: id, description: id, run: run };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('| DyNTS_Migration_Runner', () => {
|
|
10
|
+
|
|
11
|
+
describe('| selectPending (pure)', () => {
|
|
12
|
+
it('| kiszűri a már-alkalmazottakat, megőrzi a sorrendet', () => {
|
|
13
|
+
const migs: DyNTS_MigrationEntry[] = [
|
|
14
|
+
entry('a', async () => ({ touched: 0 })),
|
|
15
|
+
entry('b', async () => ({ touched: 0 })),
|
|
16
|
+
entry('c', async () => ({ touched: 0 })),
|
|
17
|
+
];
|
|
18
|
+
const pending: DyNTS_MigrationEntry[] = DyNTS_Migration_Runner.selectPending(migs, new Set([ 'b' ]));
|
|
19
|
+
expect(pending.map((m) => m.id)).toEqual([ 'a', 'c' ]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('| mind alkalmazva → üres; üres registry → üres', () => {
|
|
23
|
+
const migs: DyNTS_MigrationEntry[] = [ entry('a', async () => ({ touched: 0 })) ];
|
|
24
|
+
expect(DyNTS_Migration_Runner.selectPending(migs, new Set([ 'a' ])).length).toBe(0);
|
|
25
|
+
expect(DyNTS_Migration_Runner.selectPending([], new Set()).length).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('| runPending (mock ledger)', () => {
|
|
30
|
+
let runner: DyNTS_Migration_Runner;
|
|
31
|
+
let saved: DyNTS_SchemaMigration[];
|
|
32
|
+
let appliedRecords: DyNTS_SchemaMigration[];
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
runner = DyNTS_Migration_Runner.getInstance();
|
|
36
|
+
saved = [];
|
|
37
|
+
appliedRecords = [];
|
|
38
|
+
|
|
39
|
+
const fakeLedger: any = { // ccap-review-disable-line no-any-type
|
|
40
|
+
dataDBService: { find: async (): Promise<DyNTS_SchemaMigration[]> => appliedRecords },
|
|
41
|
+
data: null,
|
|
42
|
+
saveData: async function (): Promise<void> { saved.push(this.data); appliedRecords.push(this.data); },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
spyOn(runner as any, 'createLedgerDataService').and.returnValue(fakeLedger); // ccap-review-disable-line no-any-type
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('| a pending-et futtatja + rögzíti (id+version+touched), a már-alkalmazottat SKIP-eli', async () => {
|
|
49
|
+
appliedRecords.push(new DyNTS_SchemaMigration({ migrationId: 'm1' }));
|
|
50
|
+
const ran: string[] = [];
|
|
51
|
+
|
|
52
|
+
const migs: DyNTS_MigrationEntry[] = [
|
|
53
|
+
entry('m1', async () => { ran.push('m1'); return { touched: 1 }; }),
|
|
54
|
+
entry('m2', async () => { ran.push('m2'); return { touched: 5 }; }),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const res = await runner.runPending(migs, '1.0.0');
|
|
58
|
+
|
|
59
|
+
expect(ran).toEqual([ 'm2' ]); // m1 skip (már applied), m2 fut
|
|
60
|
+
expect(res.applied).toBe(1);
|
|
61
|
+
expect(res.skipped).toBe(1);
|
|
62
|
+
expect(saved[0].migrationId).toBe('m2');
|
|
63
|
+
expect(saved[0].appliedVersion).toBe('1.0.0');
|
|
64
|
+
expect(saved[0].notes).toBe('touched=5');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('| RUN-ONCE: második sweep-en (m2 már a ledgerben) semmi nem fut', async () => {
|
|
68
|
+
appliedRecords.push(new DyNTS_SchemaMigration({ migrationId: 'm2' }));
|
|
69
|
+
const ran: string[] = [];
|
|
70
|
+
const migs: DyNTS_MigrationEntry[] = [ entry('m2', async () => { ran.push('m2'); return { touched: 0 }; }) ];
|
|
71
|
+
|
|
72
|
+
const res = await runner.runPending(migs, '1.0.0');
|
|
73
|
+
expect(ran.length).toBe(0);
|
|
74
|
+
expect(res.applied).toBe(0);
|
|
75
|
+
expect(res.skipped).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('| BOOT-SAFE: egy hibás migráció NEM dob + NEM állítja meg a többit', async () => {
|
|
79
|
+
const ran: string[] = [];
|
|
80
|
+
const migs: DyNTS_MigrationEntry[] = [
|
|
81
|
+
entry('bad', async () => { throw new Error('boom'); }),
|
|
82
|
+
entry('good', async () => { ran.push('good'); return { touched: 2 }; }),
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const res = await runner.runPending(migs, '1.0.0');
|
|
86
|
+
expect(res.failed).toBe(1);
|
|
87
|
+
expect(res.applied).toBe(1); // 'good' a hiba UTÁN is lefutott
|
|
88
|
+
expect(ran).toEqual([ 'good' ]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|