@futdevpro/nts-dynamo 1.15.155 → 1.15.157

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.
Files changed (48) hide show
  1. package/README.md +10 -10
  2. package/__documentations/2026-08-22-bfr-ctm-018-content-free-endpoint-errors.md +54 -0
  3. package/__documentations/2026-08-22-bfr-ctm-020-content-free-request-telemetry.md +65 -0
  4. package/__documentations/2026-08-22-content-free-process-error-boundary.md +21 -1
  5. package/build/_models/control-models/endpoint-params.control-model.d.ts +6 -0
  6. package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
  7. package/build/_models/control-models/endpoint-params.control-model.js +71 -146
  8. package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
  9. package/build/_models/interfaces/request-telemetry-settings.interface.d.ts +10 -0
  10. package/build/_models/interfaces/request-telemetry-settings.interface.d.ts.map +1 -0
  11. package/build/_models/interfaces/request-telemetry-settings.interface.js +3 -0
  12. package/build/_models/interfaces/request-telemetry-settings.interface.js.map +1 -0
  13. package/build/_models/interfaces/request-telemetry-snapshot.interface.d.ts +29 -0
  14. package/build/_models/interfaces/request-telemetry-snapshot.interface.d.ts.map +1 -0
  15. package/build/_models/interfaces/request-telemetry-snapshot.interface.js +3 -0
  16. package/build/_models/interfaces/request-telemetry-snapshot.interface.js.map +1 -0
  17. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts +1 -0
  18. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts.map +1 -1
  19. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js +1 -0
  20. package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js.map +1 -1
  21. package/build/_services/core/request-telemetry.control-service.d.ts +51 -0
  22. package/build/_services/core/request-telemetry.control-service.d.ts.map +1 -0
  23. package/build/_services/core/request-telemetry.control-service.js +286 -0
  24. package/build/_services/core/request-telemetry.control-service.js.map +1 -0
  25. package/build/_services/route/routing-module.service.d.ts.map +1 -1
  26. package/build/_services/route/routing-module.service.js +13 -10
  27. package/build/_services/route/routing-module.service.js.map +1 -1
  28. package/build/_services/server/app.server.d.ts +5 -0
  29. package/build/_services/server/app.server.d.ts.map +1 -1
  30. package/build/_services/server/app.server.js +13 -0
  31. package/build/_services/server/app.server.js.map +1 -1
  32. package/build/index.d.ts +3 -0
  33. package/build/index.d.ts.map +1 -1
  34. package/build/index.js +3 -0
  35. package/build/index.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/_models/control-models/endpoint-params.control-model.spec.ts +87 -2
  38. package/src/_models/control-models/endpoint-params.control-model.ts +93 -182
  39. package/src/_models/interfaces/request-telemetry-settings.interface.ts +9 -0
  40. package/src/_models/interfaces/request-telemetry-snapshot.interface.ts +30 -0
  41. package/src/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.ts +1 -0
  42. package/src/_services/core/request-telemetry.control-service.spec.ts +217 -0
  43. package/src/_services/core/request-telemetry.control-service.ts +381 -0
  44. package/src/_services/route/routing-module.service.ts +27 -18
  45. package/src/_services/server/app.server.ts +29 -9
  46. package/src/_services/shared.static-service.spec.ts +57 -57
  47. package/src/_services/shared.static-service.ts +48 -48
  48. package/src/index.ts +5 -2
@@ -0,0 +1,381 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { NextFunction, Request, RequestHandler, Response } from 'express';
3
+
4
+ import { DyNTS_resolveExposeHeaders } from '../../_collections/app-version-header.util';
5
+ import {
6
+ DyNTS_RequestTelemetry_Settings
7
+ } from '../../_models/interfaces/request-telemetry-settings.interface';
8
+ import {
9
+ DyNTS_RequestDurationBucket_Interface,
10
+ DyNTS_RequestRouteMetric_Interface,
11
+ DyNTS_RequestTelemetrySnapshot_Interface
12
+ } from '../../_models/interfaces/request-telemetry-snapshot.interface';
13
+
14
+ const correlationHeader: 'X-Correlation-Id' = 'X-Correlation-Id';
15
+ const contractVersion: 'dynts-request-telemetry/1' = 'dynts-request-telemetry/1';
16
+ const defaultDurationBuckets: number[] = [ 25, 100, 250, 1000, 5000 ];
17
+ const defaultMaxRouteCardinality: number = 128;
18
+ const otherRouteTemplate: string = '__other__';
19
+ const unmatchedRouteTemplate: string = '__unmatched__';
20
+ const allowedMethods: ReadonlySet<string> = new Set([ 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT' ]);
21
+ const routeTemplatePattern: RegExp = /^\/[A-Za-z0-9_:/.*-]{0,159}$/;
22
+
23
+ interface DyNTS_RequestTelemetryResolvedSettings_Interface {
24
+ enabled: boolean;
25
+ maxRouteCardinality: number;
26
+ durationBucketUpperBoundsMs: number[];
27
+ }
28
+
29
+ interface DyNTS_RequestTelemetryState_Interface {
30
+ startedAtMs: number;
31
+ routeTemplate: string;
32
+ correlationId: string;
33
+ }
34
+
35
+ interface DyNTS_RequestRouteAccumulator_Interface extends DyNTS_RequestRouteMetric_Interface {}
36
+
37
+ interface DyNTS_RequestRouteObservation_Interface {
38
+ method: string;
39
+ routeTemplate: string;
40
+ statusCode: number;
41
+ durationMs: number;
42
+ aborted: boolean;
43
+ }
44
+
45
+ /**
46
+ * Event-driven, content-free request correlation and aggregate metrics.
47
+ *
48
+ * The service never reads or stores URL, query, headers, body, IP, user agent, account identity or error content.
49
+ * Route labels can only come from the framework's declared route templates. Caller-provided correlation values are
50
+ * ignored and replaced with a server-generated UUID.
51
+ */
52
+ export class DyNTS_RequestTelemetry_ControlService {
53
+ private static instance: DyNTS_RequestTelemetry_ControlService;
54
+
55
+ /** Returns the process-wide request telemetry accumulator used by open and secure Express applications. */
56
+ public static getInstance(): DyNTS_RequestTelemetry_ControlService {
57
+ if (!DyNTS_RequestTelemetry_ControlService.instance) {
58
+ DyNTS_RequestTelemetry_ControlService.instance = new DyNTS_RequestTelemetry_ControlService();
59
+ }
60
+
61
+ return DyNTS_RequestTelemetry_ControlService.instance;
62
+ }
63
+
64
+ private settings: DyNTS_RequestTelemetryResolvedSettings_Interface = this.resolveSettings({});
65
+ private configurationFingerprint: string | null = null;
66
+ private requestStates: WeakMap<Request, DyNTS_RequestTelemetryState_Interface> = new WeakMap();
67
+ private readonly routeMetrics: Map<string, DyNTS_RequestRouteAccumulator_Interface> = new Map();
68
+ private readonly retainedRouteTemplates: Set<string> = new Set();
69
+ private durationCounts: number[] = this.settings.durationBucketUpperBoundsMs.map((): number => 0);
70
+ private overflowDurationCount: number = 0;
71
+ private readonly statusClassCounts: Record<string, number> = {
72
+ '1xx': 0,
73
+ '2xx': 0,
74
+ '3xx': 0,
75
+ '4xx': 0,
76
+ '5xx': 0,
77
+ other: 0,
78
+ };
79
+ private requestsStarted: number = 0;
80
+ private requestsCompleted: number = 0;
81
+ private requestsAborted: number = 0;
82
+ private inFlight: number = 0;
83
+ private droppedRouteTemplates: number = 0;
84
+
85
+ private constructor() {}
86
+
87
+ /** Installs one immutable configuration; repeated identical calls are idempotent. */
88
+ public configure(settings: DyNTS_RequestTelemetry_Settings): void {
89
+ const resolved: DyNTS_RequestTelemetryResolvedSettings_Interface = this.resolveSettings(settings);
90
+ const fingerprint: string = JSON.stringify(resolved);
91
+
92
+ if (this.configurationFingerprint && this.configurationFingerprint !== fingerprint) {
93
+ throw new Error('DYNTS|REQUEST_TELEMETRY|CONFIGURATION_CONFLICT');
94
+ }
95
+
96
+ if (this.configurationFingerprint === fingerprint) {
97
+ return;
98
+ }
99
+
100
+ this.settings = resolved;
101
+ this.configurationFingerprint = fingerprint;
102
+ this.durationCounts = resolved.durationBucketUpperBoundsMs.map((): number => 0);
103
+ }
104
+
105
+ /** Returns the early Express middleware that creates correlation and completion listeners. */
106
+ public createMiddleware(): RequestHandler {
107
+ return (req: Request, res: Response, next: NextFunction): void => {
108
+ if (!this.settings.enabled) {
109
+ next();
110
+
111
+ return;
112
+ }
113
+
114
+ try {
115
+ this.beginRequest(req, res);
116
+ } catch {
117
+ // Telemetry is observation-only and must never change application response semantics.
118
+ }
119
+
120
+ next();
121
+ };
122
+ }
123
+
124
+ /** Returns a route-local middleware that labels only a framework-declared route template. */
125
+ public identifyRoute(routeTemplate: string): RequestHandler {
126
+ const safeRouteTemplate: string = this.normalizeRouteTemplate(routeTemplate);
127
+
128
+ return (req: Request, _res: Response, next: NextFunction): void => {
129
+ const state: DyNTS_RequestTelemetryState_Interface | undefined = this.requestStates.get(req);
130
+
131
+ if (state) {
132
+ state.routeTemplate = safeRouteTemplate;
133
+ }
134
+
135
+ next();
136
+ };
137
+ }
138
+
139
+ /** Returns the generated correlation ID for internal error/audit joins without reading request headers. */
140
+ public getCorrelationId(req: Request): string | undefined {
141
+ return this.requestStates.get(req)?.correlationId;
142
+ }
143
+
144
+ /** Returns a stable copy of all bounded aggregate counters. */
145
+ public snapshot(): DyNTS_RequestTelemetrySnapshot_Interface {
146
+ const durationBuckets: DyNTS_RequestDurationBucket_Interface[] = this.settings.durationBucketUpperBoundsMs.map(
147
+ (upperBoundMs: number, index: number): DyNTS_RequestDurationBucket_Interface => ({
148
+ upperBoundMs: upperBoundMs,
149
+ count: this.durationCounts[index],
150
+ })
151
+ );
152
+
153
+ durationBuckets.push({ upperBoundMs: null, count: this.overflowDurationCount });
154
+
155
+ return {
156
+ contractVersion: contractVersion,
157
+ correlationHeader: correlationHeader,
158
+ requestsStarted: this.requestsStarted,
159
+ requestsCompleted: this.requestsCompleted,
160
+ requestsAborted: this.requestsAborted,
161
+ inFlight: this.inFlight,
162
+ droppedRouteTemplates: this.droppedRouteTemplates,
163
+ statusClasses: { ...this.statusClassCounts },
164
+ durationBuckets: durationBuckets,
165
+ routes: [ ...this.routeMetrics.values() ]
166
+ .map((metric: DyNTS_RequestRouteAccumulator_Interface): DyNTS_RequestRouteMetric_Interface => ({
167
+ ...metric,
168
+ }))
169
+ .sort((first: DyNTS_RequestRouteMetric_Interface, second: DyNTS_RequestRouteMetric_Interface): number =>
170
+ `${first.method}:${first.routeTemplate}`.localeCompare(`${second.method}:${second.routeTemplate}`)
171
+ ),
172
+ };
173
+ }
174
+
175
+ /** Test-only reset for the process-wide singleton. */
176
+ public _resetForTesting(): void {
177
+ this.settings = this.resolveSettings({});
178
+ this.configurationFingerprint = null;
179
+ this.requestStates = new WeakMap();
180
+ this.routeMetrics.clear();
181
+ this.retainedRouteTemplates.clear();
182
+ this.durationCounts = this.settings.durationBucketUpperBoundsMs.map((): number => 0);
183
+ this.overflowDurationCount = 0;
184
+ Object.keys(this.statusClassCounts).forEach((key: string): void => {
185
+ this.statusClassCounts[key] = 0;
186
+ });
187
+ this.requestsStarted = 0;
188
+ this.requestsCompleted = 0;
189
+ this.requestsAborted = 0;
190
+ this.inFlight = 0;
191
+ this.droppedRouteTemplates = 0;
192
+ }
193
+
194
+ private beginRequest(req: Request, res: Response): void {
195
+ const state: DyNTS_RequestTelemetryState_Interface = {
196
+ startedAtMs: Date.now(),
197
+ routeTemplate: unmatchedRouteTemplate,
198
+ correlationId: randomUUID(),
199
+ };
200
+ const existingExposeHeader: string | number | string[] | undefined =
201
+ res.getHeader('Access-Control-Expose-Headers');
202
+
203
+ res.setHeader(correlationHeader, state.correlationId);
204
+ res.setHeader(
205
+ 'Access-Control-Expose-Headers',
206
+ DyNTS_resolveExposeHeaders(
207
+ typeof existingExposeHeader === 'string' ? existingExposeHeader : undefined,
208
+ correlationHeader
209
+ )
210
+ );
211
+
212
+ let settled: boolean = false;
213
+ /** Settles one response exactly once and removes both lifecycle listeners. */
214
+ const settle: (aborted: boolean) => void = (aborted: boolean): void => {
215
+ if (settled) {
216
+ return;
217
+ }
218
+ settled = true;
219
+ res.off('finish', onFinish);
220
+ res.off('close', onClose);
221
+
222
+ try {
223
+ this.completeRequest(req, res, aborted);
224
+ } catch {
225
+ // Telemetry is observation-only and must never surface a response-lifecycle failure.
226
+ }
227
+ };
228
+ /** Records a normally completed response. */
229
+ const onFinish: () => void = (): void => settle(false);
230
+ /** Records a connection closed before a normal finish. */
231
+ const onClose: () => void = (): void => settle(true);
232
+
233
+ try {
234
+ res.once('finish', onFinish);
235
+ res.once('close', onClose);
236
+ } catch (error: unknown) {
237
+ res.off('finish', onFinish);
238
+ res.off('close', onClose);
239
+
240
+ throw error;
241
+ }
242
+
243
+ this.requestStates.set(req, state);
244
+ this.requestsStarted = this.safeIncrement(this.requestsStarted);
245
+ this.inFlight = this.safeIncrement(this.inFlight);
246
+ }
247
+
248
+ private completeRequest(req: Request, res: Response, aborted: boolean): void {
249
+ const state: DyNTS_RequestTelemetryState_Interface | undefined = this.requestStates.get(req);
250
+
251
+ if (!state) {
252
+ return;
253
+ }
254
+
255
+ this.requestStates.delete(req);
256
+ this.inFlight = Math.max(0, this.inFlight - 1);
257
+ const durationMs: number = Math.max(0, Date.now() - state.startedAtMs);
258
+
259
+ this.recordDuration(durationMs);
260
+
261
+ if (aborted) {
262
+ this.requestsAborted = this.safeIncrement(this.requestsAborted);
263
+ } else {
264
+ this.requestsCompleted = this.safeIncrement(this.requestsCompleted);
265
+ this.recordStatusClass(res.statusCode);
266
+ }
267
+
268
+ this.recordRouteMetric({
269
+ method: req.method,
270
+ routeTemplate: state.routeTemplate,
271
+ statusCode: res.statusCode,
272
+ durationMs: durationMs,
273
+ aborted: aborted,
274
+ });
275
+ }
276
+
277
+ private recordDuration(durationMs: number): void {
278
+ const bucketIndex: number = this.settings.durationBucketUpperBoundsMs.findIndex(
279
+ (upperBoundMs: number): boolean => durationMs <= upperBoundMs
280
+ );
281
+
282
+ if (bucketIndex === -1) {
283
+ this.overflowDurationCount = this.safeIncrement(this.overflowDurationCount);
284
+
285
+ return;
286
+ }
287
+
288
+ this.durationCounts[bucketIndex] = this.safeIncrement(this.durationCounts[bucketIndex]);
289
+ }
290
+
291
+ private recordStatusClass(statusCode: number): void {
292
+ const statusClass: string = Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599
293
+ ? `${Math.floor(statusCode / 100)}xx`
294
+ : 'other';
295
+
296
+ this.statusClassCounts[statusClass] = this.safeIncrement(this.statusClassCounts[statusClass]);
297
+ }
298
+
299
+ private recordRouteMetric(observation: DyNTS_RequestRouteObservation_Interface): void {
300
+ const safeMethod: string = allowedMethods.has(observation.method) ? observation.method : 'OTHER';
301
+ let safeRouteTemplate: string = observation.routeTemplate;
302
+ let routeKey: string = `${safeMethod}:${safeRouteTemplate}`;
303
+ let metric: DyNTS_RequestRouteAccumulator_Interface | undefined = this.routeMetrics.get(routeKey);
304
+
305
+ if (safeRouteTemplate === otherRouteTemplate) {
306
+ this.droppedRouteTemplates = this.safeIncrement(this.droppedRouteTemplates);
307
+ } else if (safeRouteTemplate !== unmatchedRouteTemplate && !this.retainedRouteTemplates.has(safeRouteTemplate)) {
308
+ if (this.retainedRouteTemplates.size >= this.settings.maxRouteCardinality) {
309
+ this.droppedRouteTemplates = this.safeIncrement(this.droppedRouteTemplates);
310
+ safeRouteTemplate = otherRouteTemplate;
311
+ routeKey = `${safeMethod}:${safeRouteTemplate}`;
312
+ metric = this.routeMetrics.get(routeKey);
313
+ } else {
314
+ this.retainedRouteTemplates.add(safeRouteTemplate);
315
+ }
316
+ }
317
+
318
+ if (!metric) {
319
+ metric = {
320
+ method: safeMethod,
321
+ routeTemplate: safeRouteTemplate,
322
+ count: 0,
323
+ abortedCount: 0,
324
+ serverErrorCount: 0,
325
+ totalDurationMs: 0,
326
+ maxDurationMs: 0,
327
+ };
328
+ this.routeMetrics.set(routeKey, metric);
329
+ }
330
+
331
+ metric.count = this.safeIncrement(metric.count);
332
+ metric.abortedCount = observation.aborted ? this.safeIncrement(metric.abortedCount) : metric.abortedCount;
333
+ metric.serverErrorCount = !observation.aborted && observation.statusCode >= 500 && observation.statusCode <= 599
334
+ ? this.safeIncrement(metric.serverErrorCount)
335
+ : metric.serverErrorCount;
336
+ metric.totalDurationMs = this.safeAdd(metric.totalDurationMs, observation.durationMs);
337
+ metric.maxDurationMs = Math.max(metric.maxDurationMs, observation.durationMs);
338
+ }
339
+
340
+ private normalizeRouteTemplate(routeTemplate: string): string {
341
+ return routeTemplatePattern.test(routeTemplate) ? routeTemplate : otherRouteTemplate;
342
+ }
343
+
344
+ private resolveSettings(
345
+ settings: DyNTS_RequestTelemetry_Settings
346
+ ): DyNTS_RequestTelemetryResolvedSettings_Interface {
347
+ const maxRouteCardinality: number = settings.maxRouteCardinality ?? defaultMaxRouteCardinality;
348
+ const durationBucketUpperBoundsMs: number[] = settings.durationBucketUpperBoundsMs ?? defaultDurationBuckets;
349
+
350
+ if (!Number.isInteger(maxRouteCardinality) || maxRouteCardinality < 1 || maxRouteCardinality > 1000) {
351
+ throw new Error('DYNTS|REQUEST_TELEMETRY|MAX_ROUTE_CARDINALITY_INVALID');
352
+ }
353
+
354
+ if (
355
+ !durationBucketUpperBoundsMs.length ||
356
+ durationBucketUpperBoundsMs.length > 16 ||
357
+ durationBucketUpperBoundsMs.some(
358
+ (value: number): boolean => !Number.isInteger(value) || value < 1 || value > 3_600_000
359
+ ) ||
360
+ durationBucketUpperBoundsMs.some(
361
+ (value: number, index: number): boolean => index > 0 && value <= durationBucketUpperBoundsMs[index - 1]
362
+ )
363
+ ) {
364
+ throw new Error('DYNTS|REQUEST_TELEMETRY|DURATION_BUCKETS_INVALID');
365
+ }
366
+
367
+ return {
368
+ enabled: settings.enabled === true,
369
+ maxRouteCardinality: maxRouteCardinality,
370
+ durationBucketUpperBoundsMs: [ ...durationBucketUpperBoundsMs ],
371
+ };
372
+ }
373
+
374
+ private safeIncrement(value: number): number {
375
+ return value >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : value + 1;
376
+ }
377
+
378
+ private safeAdd(first: number, second: number): number {
379
+ return first >= Number.MAX_SAFE_INTEGER - second ? Number.MAX_SAFE_INTEGER : first + second;
380
+ }
381
+ }
@@ -16,7 +16,8 @@ import {
16
16
  import {
17
17
  DyNTS_RoutingModule_Settings
18
18
  } from '../../_models/interfaces/routing-module-settings.interface';
19
- import { DyNTS_Controller } from './controller.service';
19
+ import { DyNTS_Controller } from './controller.service';
20
+ import { DyNTS_RequestTelemetry_ControlService } from '../core/request-telemetry.control-service';
20
21
 
21
22
  /**
22
23
  * Routing Module
@@ -226,26 +227,30 @@ export class DyNTS_RoutingModule {
226
227
  *
227
228
  * @param endpointParams
228
229
  */
229
- private mountOpenRoute(endpointParams: DyNTS_Endpoint_Params): void {
230
- switch (endpointParams.type) {
231
- case DyFM_HttpCallType.get:
232
- this.openRouter.get(endpointParams.endpoint, endpointParams.getFullExecution());
233
- break;
230
+ private mountOpenRoute(endpointParams: DyNTS_Endpoint_Params): void {
231
+ const routeIdentity = DyNTS_RequestTelemetry_ControlService.getInstance().identifyRoute(
232
+ `${this.route}${endpointParams.endpoint}`
233
+ );
234
+
235
+ switch (endpointParams.type) {
236
+ case DyFM_HttpCallType.get:
237
+ this.openRouter.get(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
238
+ break;
234
239
 
235
240
  case DyFM_HttpCallType.post:
236
- this.openRouter.post(endpointParams.endpoint, endpointParams.getFullExecution());
241
+ this.openRouter.post(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
237
242
  break;
238
243
 
239
244
  case DyFM_HttpCallType.put:
240
- this.openRouter.put(endpointParams.endpoint, endpointParams.getFullExecution());
245
+ this.openRouter.put(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
241
246
  break;
242
247
 
243
248
  case DyFM_HttpCallType.patch:
244
- this.openRouter.patch(endpointParams.endpoint, endpointParams.getFullExecution());
249
+ this.openRouter.patch(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
245
250
  break;
246
251
 
247
252
  case DyFM_HttpCallType.delete:
248
- this.openRouter.delete(endpointParams.endpoint, endpointParams.getFullExecution());
253
+ this.openRouter.delete(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
249
254
  break;
250
255
 
251
256
  default:
@@ -269,26 +274,30 @@ export class DyNTS_RoutingModule {
269
274
  *
270
275
  * @param endpointParams
271
276
  */
272
- private mountSecureRoute(endpointParams: DyNTS_Endpoint_Params): void {
273
- switch (endpointParams.type) {
274
- case DyFM_HttpCallType.get:
275
- this.secureRouter.get(endpointParams.endpoint, endpointParams.getFullExecution());
277
+ private mountSecureRoute(endpointParams: DyNTS_Endpoint_Params): void {
278
+ const routeIdentity = DyNTS_RequestTelemetry_ControlService.getInstance().identifyRoute(
279
+ `${this.route}${endpointParams.endpoint}`
280
+ );
281
+
282
+ switch (endpointParams.type) {
283
+ case DyFM_HttpCallType.get:
284
+ this.secureRouter.get(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
276
285
  break;
277
286
 
278
287
  case DyFM_HttpCallType.post:
279
- this.secureRouter.post(endpointParams.endpoint, endpointParams.getFullExecution());
288
+ this.secureRouter.post(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
280
289
  break;
281
290
 
282
291
  case DyFM_HttpCallType.put:
283
- this.secureRouter.put(endpointParams.endpoint, endpointParams.getFullExecution());
292
+ this.secureRouter.put(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
284
293
  break;
285
294
 
286
295
  case DyFM_HttpCallType.patch:
287
- this.secureRouter.patch(endpointParams.endpoint, endpointParams.getFullExecution());
296
+ this.secureRouter.patch(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
288
297
  break;
289
298
 
290
299
  case DyFM_HttpCallType.delete:
291
- this.secureRouter.delete(endpointParams.endpoint, endpointParams.getFullExecution());
300
+ this.secureRouter.delete(endpointParams.endpoint, routeIdentity, endpointParams.getFullExecution());
292
301
  break;
293
302
 
294
303
  default:
@@ -35,7 +35,8 @@ import { DyNTS_getEnvironment_settings } from '../../_collections/get-environmen
35
35
  import { DyNTS_createSecurityHeadersMiddleware } from '../../_collections/security-headers.util';
36
36
  import { DyNTS_createLoadShedMiddleware } from '../../_collections/load-shed.util';
37
37
  import { DyNTS_createAppVersionHeaderMiddleware } from '../../_collections/app-version-header.util';
38
- import { DyNTS_AppVersionHeader_Settings } from '../../_models/interfaces/app-version-header-settings.interface';
38
+ import { DyNTS_AppVersionHeader_Settings } from '../../_models/interfaces/app-version-header-settings.interface';
39
+ import { DyNTS_RequestTelemetry_Settings } from '../../_models/interfaces/request-telemetry-settings.interface';
39
40
  import { DyNTS_SecurityHeaders_Settings } from '../../_models/interfaces/security-headers-settings.interface';
40
41
  import { startMongoReconnectGuard } from '../../_collections/mongo-reconnect-guard.util';
41
42
  import { DyNTS_resolveStaticCacheControlHeader } from '../../_collections/static-cache-control.util';
@@ -65,7 +66,8 @@ import { DyNTS_SingletonService } from '../base/singleton.service';
65
66
  import { DyNTS_GlobalService } from '../core/global.service';
66
67
  import { DyNTS_CollectionGrowthMonitor } from '../core/collection-growth-monitor.service';
67
68
  import { DyNTS_MemoryGuard } from '../core/memory-guard.service';
68
- import { DyNTS_EventLoopDiag } from '../core/event-loop-diag.service';
69
+ import { DyNTS_EventLoopDiag } from '../core/event-loop-diag.service';
70
+ import { DyNTS_RequestTelemetry_ControlService } from '../core/request-telemetry.control-service';
69
71
  import { DyNTS_RoutingModule } from '../route/routing-module.service';
70
72
  import { DyNTS_getStarRoute } from '../../_collections/star.controller';
71
73
  import { DyNTS_SafeDiagnosticStage_Type } from '../../_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum';
@@ -1261,7 +1263,8 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1261
1263
  this.mountCors(this.openExpress);
1262
1264
  // FR-292 — a deployolt app-verzio minden valaszon. A CORS UTAN kell, mert az
1263
1265
  // `Access-Control-Expose-Headers`-hoz HOZZAFUZ (kulonben a CORS felulirna).
1264
- this.mountAppVersionHeader(this.openExpress);
1266
+ this.mountAppVersionHeader(this.openExpress);
1267
+ this.mountRequestTelemetry(this.openExpress);
1265
1268
  // BFR-OVERSEER-009 — opt-in auto-load-shed 503 CRITICAL memória-nyomásnál (esszenciális utak kivételével).
1266
1269
  if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) {
1267
1270
  this.openExpress.use(DyNTS_createLoadShedMiddleware());
@@ -1281,7 +1284,8 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1281
1284
  // FR-041 — CORS allowlist enforcement (same as open express).
1282
1285
  this.mountCors(this.secureExpress);
1283
1286
  // FR-292 — a deployolt app-verzio minden valaszon (mint az open expressen).
1284
- this.mountAppVersionHeader(this.secureExpress);
1287
+ this.mountAppVersionHeader(this.secureExpress);
1288
+ this.mountRequestTelemetry(this.secureExpress);
1285
1289
  // BFR-OVERSEER-009 — opt-in auto-load-shed 503 (mint az open expressen).
1286
1290
  if (DyNTS_global_settings.memoryGuard?.loadShedEnabled) {
1287
1291
  this.secureExpress.use(DyNTS_createLoadShedMiddleware());
@@ -1299,7 +1303,7 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1299
1303
  /**
1300
1304
  *
1301
1305
  */
1302
- private async startExpresses(): Promise<void> {
1306
+ private async startExpresses(): Promise<void> {
1303
1307
  if (this.fnLogs && this.deepLog) DyFM_Log.log('\nfn:. startExpresses');
1304
1308
 
1305
1309
  try {
@@ -1644,7 +1648,7 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1644
1648
  * informativ response-header, ami se PII-t, se belso reszletet nem szivarogtat, viszont
1645
1649
  * enelkul minden app kulon-kulon epitene ugyanezt.
1646
1650
  */
1647
- protected mountAppVersionHeader(express: Express.Application): void {
1651
+ protected mountAppVersionHeader(express: Express.Application): void {
1648
1652
  const settings: DyNTS_AppVersionHeader_Settings | undefined = DyNTS_global_settings.appVersionHeader;
1649
1653
  if (settings?.enabled === false) {
1650
1654
  return;
@@ -1652,8 +1656,21 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1652
1656
  express.use(DyNTS_createAppVersionHeaderMiddleware(
1653
1657
  (): string => DyNTS_global_settings.systemVersion,
1654
1658
  settings?.headerName,
1655
- ));
1656
- }
1659
+ ));
1660
+ }
1661
+
1662
+ /** Mounts opt-in correlation and aggregate metrics before load shedding and every API route. */
1663
+ protected mountRequestTelemetry(express: Express.Application): void {
1664
+ const settings: DyNTS_RequestTelemetry_Settings | undefined = this.getRequestTelemetrySettings?.();
1665
+ if (settings?.enabled !== true) {
1666
+ return;
1667
+ }
1668
+
1669
+ const telemetry: DyNTS_RequestTelemetry_ControlService =
1670
+ DyNTS_RequestTelemetry_ControlService.getInstance();
1671
+ telemetry.configure(settings);
1672
+ express.use(telemetry.createMiddleware());
1673
+ }
1657
1674
 
1658
1675
  protected mountCors(express: Express.Application): void {
1659
1676
  const settings: DyNTS_Cors_Settings | undefined = this.getCorsSettings?.();
@@ -1932,7 +1949,10 @@ export abstract class DyNTS_App extends DyNTS_SingletonService {
1932
1949
  * cross-domain auth flow miatt. OPTIONS preflight 204-gyel rövidre záródik.
1933
1950
  * Ha undefined → no CORS headers, no preflight handling — back-compat.
1934
1951
  */
1935
- getCorsSettings?(): DyNTS_Cors_Settings | undefined;
1952
+ getCorsSettings?(): DyNTS_Cors_Settings | undefined;
1953
+
1954
+ /** Enables content-free request correlation and event-driven aggregate metrics before API routing. */
1955
+ getRequestTelemetrySettings?(): DyNTS_RequestTelemetry_Settings | undefined;
1936
1956
 
1937
1957
  /**
1938
1958
  * Opcionális generikus hook: custom Express middleware regisztrálása az API route-ok