@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.
- package/README.md +10 -10
- package/__documentations/2026-08-22-bfr-ctm-018-content-free-endpoint-errors.md +54 -0
- package/__documentations/2026-08-22-bfr-ctm-020-content-free-request-telemetry.md +65 -0
- package/__documentations/2026-08-22-content-free-process-error-boundary.md +21 -1
- package/build/_models/control-models/endpoint-params.control-model.d.ts +6 -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 +71 -146
- package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
- package/build/_models/interfaces/request-telemetry-settings.interface.d.ts +10 -0
- package/build/_models/interfaces/request-telemetry-settings.interface.d.ts.map +1 -0
- package/build/_models/interfaces/request-telemetry-settings.interface.js +3 -0
- package/build/_models/interfaces/request-telemetry-settings.interface.js.map +1 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.d.ts +29 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.d.ts.map +1 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.js +3 -0
- package/build/_models/interfaces/request-telemetry-snapshot.interface.js.map +1 -0
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts +1 -0
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.d.ts.map +1 -1
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js +1 -0
- package/build/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.js.map +1 -1
- package/build/_services/core/request-telemetry.control-service.d.ts +51 -0
- package/build/_services/core/request-telemetry.control-service.d.ts.map +1 -0
- package/build/_services/core/request-telemetry.control-service.js +286 -0
- package/build/_services/core/request-telemetry.control-service.js.map +1 -0
- package/build/_services/route/routing-module.service.d.ts.map +1 -1
- package/build/_services/route/routing-module.service.js +13 -10
- package/build/_services/route/routing-module.service.js.map +1 -1
- package/build/_services/server/app.server.d.ts +5 -0
- package/build/_services/server/app.server.d.ts.map +1 -1
- package/build/_services/server/app.server.js +13 -0
- package/build/_services/server/app.server.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/_models/control-models/endpoint-params.control-model.spec.ts +87 -2
- package/src/_models/control-models/endpoint-params.control-model.ts +93 -182
- package/src/_models/interfaces/request-telemetry-settings.interface.ts +9 -0
- package/src/_models/interfaces/request-telemetry-snapshot.interface.ts +30 -0
- package/src/_modules/server/safe-diagnostic/safe-diagnostic-stage.type-enum.ts +1 -0
- package/src/_services/core/request-telemetry.control-service.spec.ts +217 -0
- package/src/_services/core/request-telemetry.control-service.ts +381 -0
- package/src/_services/route/routing-module.service.ts +27 -18
- package/src/_services/server/app.server.ts +29 -9
- package/src/_services/shared.static-service.spec.ts +57 -57
- package/src/_services/shared.static-service.ts +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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|