@backendkit-labs/auto-learning 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-CtdA-dkB.d.cts → index-CuVtbErY.d.cts} +44 -18
- package/dist/{index-CtdA-dkB.d.ts → index-CuVtbErY.d.ts} +44 -18
- package/dist/index.cjs +193 -103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -8
- package/dist/index.d.ts +21 -8
- package/dist/index.js +193 -103
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +193 -103
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +193 -103
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +1 -1
package/dist/nestjs/index.js
CHANGED
|
@@ -48,8 +48,8 @@ var PatternRegistry = class {
|
|
|
48
48
|
});
|
|
49
49
|
return ok(void 0);
|
|
50
50
|
}
|
|
51
|
-
getAggregates(windowMinutes) {
|
|
52
|
-
const result = this.storage.getAggregates(windowMinutes);
|
|
51
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
52
|
+
const result = this.storage.getAggregates(windowMinutes, windowEnd);
|
|
53
53
|
if (!result.ok) {
|
|
54
54
|
this.observability.error("Failed to get aggregates", { error: result.error });
|
|
55
55
|
return fail(storageError("Failed to get aggregates", result.error));
|
|
@@ -83,12 +83,18 @@ var PatternRegistry = class {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
const uniqueEndpoints = new Set(all.map((p) => `${p.method}:${p.path}`));
|
|
86
|
-
|
|
86
|
+
let oldest = all[0].timestamp.getTime();
|
|
87
|
+
let newest = oldest;
|
|
88
|
+
for (const p of all) {
|
|
89
|
+
const t = p.timestamp.getTime();
|
|
90
|
+
if (t < oldest) oldest = t;
|
|
91
|
+
if (t > newest) newest = t;
|
|
92
|
+
}
|
|
87
93
|
return ok({
|
|
88
94
|
totalPatterns: all.length,
|
|
89
95
|
uniqueEndpoints: uniqueEndpoints.size,
|
|
90
|
-
oldestPattern: new Date(
|
|
91
|
-
newestPattern: new Date(
|
|
96
|
+
oldestPattern: new Date(oldest),
|
|
97
|
+
newestPattern: new Date(newest)
|
|
92
98
|
});
|
|
93
99
|
}
|
|
94
100
|
};
|
|
@@ -128,23 +134,20 @@ var AnomalyDetector = class {
|
|
|
128
134
|
});
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
|
-
if (current.statusCode >= 500 && baseline.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
detectedAt: /* @__PURE__ */ new Date()
|
|
144
|
-
});
|
|
145
|
-
}
|
|
137
|
+
if (current.statusCode >= 500 && baseline.errorRate < this.config.errorRateThreshold) {
|
|
138
|
+
reports.push({
|
|
139
|
+
id: uuid(),
|
|
140
|
+
endpoint: current.path,
|
|
141
|
+
method: current.method,
|
|
142
|
+
severity: "high",
|
|
143
|
+
metric: "error_rate",
|
|
144
|
+
expectedValue: baseline.errorRate,
|
|
145
|
+
actualValue: 1,
|
|
146
|
+
deviation: 1 / Math.max(baseline.errorRate, 1e-3),
|
|
147
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
148
|
+
});
|
|
146
149
|
}
|
|
147
|
-
return ok2(reports
|
|
150
|
+
return ok2(reports);
|
|
148
151
|
} catch (e) {
|
|
149
152
|
return fail2(
|
|
150
153
|
anomalyDetectionFailed(
|
|
@@ -160,11 +163,13 @@ var AnomalyDetector = class {
|
|
|
160
163
|
baselineMap.set(`${b.method}:${b.path}`, b);
|
|
161
164
|
}
|
|
162
165
|
const reports = [];
|
|
166
|
+
const seenUnknown = /* @__PURE__ */ new Set();
|
|
163
167
|
for (const pattern of windowPatterns) {
|
|
164
168
|
const key = `${pattern.method}:${pattern.path}`;
|
|
165
169
|
const baseline = baselineMap.get(key);
|
|
166
170
|
if (!baseline) {
|
|
167
|
-
if (this.config.enableUnknownEndpointDetection) {
|
|
171
|
+
if (this.config.enableUnknownEndpointDetection && !seenUnknown.has(key)) {
|
|
172
|
+
seenUnknown.add(key);
|
|
168
173
|
reports.push({
|
|
169
174
|
id: uuid(),
|
|
170
175
|
endpoint: pattern.path,
|
|
@@ -180,8 +185,8 @@ var AnomalyDetector = class {
|
|
|
180
185
|
continue;
|
|
181
186
|
}
|
|
182
187
|
const result = this.analyze(pattern, baseline);
|
|
183
|
-
if (result.ok
|
|
184
|
-
reports.push(result.value);
|
|
188
|
+
if (result.ok) {
|
|
189
|
+
reports.push(...result.value);
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
return ok2(reports);
|
|
@@ -209,7 +214,8 @@ var DEFAULT_TUNER_CONFIG = {
|
|
|
209
214
|
minTimeoutMs: 1e3,
|
|
210
215
|
maxTimeoutMs: 3e4,
|
|
211
216
|
smoothingFactor: 0.3,
|
|
212
|
-
adjustmentStepMs: 500
|
|
217
|
+
adjustmentStepMs: 500,
|
|
218
|
+
cooldownMs: 6e4
|
|
213
219
|
};
|
|
214
220
|
|
|
215
221
|
// src/core/config-tuner/config-tuner.ts
|
|
@@ -244,48 +250,11 @@ var ConfigTuner = class {
|
|
|
244
250
|
if (aggregates.length === 0) {
|
|
245
251
|
return ok3(this.getCurrentConfig());
|
|
246
252
|
}
|
|
247
|
-
const newConfig =
|
|
248
|
-
|
|
249
|
-
bulkhead: { ...this.currentConfig.bulkhead },
|
|
250
|
-
httpClient: { ...this.currentConfig.httpClient }
|
|
251
|
-
};
|
|
252
|
-
const changedSections = /* @__PURE__ */ new Set();
|
|
253
|
-
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
254
|
-
const targetTimeout = Math.min(
|
|
255
|
-
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
256
|
-
this.config.maxTimeoutMs
|
|
257
|
-
);
|
|
258
|
-
if (Math.abs(targetTimeout - newConfig.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
259
|
-
newConfig.httpClient.timeoutMs = this.smoothValue(newConfig.httpClient.timeoutMs, targetTimeout);
|
|
260
|
-
changedSections.add("httpClient");
|
|
261
|
-
}
|
|
262
|
-
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
263
|
-
if (avgErrorRate > 0.1) {
|
|
264
|
-
newConfig.httpClient.maxRetries = Math.min(newConfig.httpClient.maxRetries + 1, 5);
|
|
265
|
-
changedSections.add("httpClient");
|
|
266
|
-
} else if (avgErrorRate < 0.01 && newConfig.httpClient.maxRetries > 1) {
|
|
267
|
-
newConfig.httpClient.maxRetries = Math.max(newConfig.httpClient.maxRetries - 1, 0);
|
|
268
|
-
changedSections.add("httpClient");
|
|
269
|
-
}
|
|
270
|
-
const criticalAnomalies = anomalies.filter(
|
|
271
|
-
(a) => a.severity === "critical" || a.severity === "high"
|
|
272
|
-
).length;
|
|
273
|
-
if (criticalAnomalies > 0) {
|
|
274
|
-
newConfig.circuitBreaker.failureThreshold = Math.max(
|
|
275
|
-
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
276
|
-
10
|
|
277
|
-
);
|
|
278
|
-
changedSections.add("circuitBreaker");
|
|
279
|
-
} else if (anomalies.length === 0) {
|
|
280
|
-
newConfig.circuitBreaker.failureThreshold = Math.min(
|
|
281
|
-
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
282
|
-
80
|
|
283
|
-
);
|
|
284
|
-
changedSections.add("circuitBreaker");
|
|
285
|
-
}
|
|
253
|
+
const newConfig = this.computeNext(aggregates, anomalies);
|
|
254
|
+
const changedSections = this.diffSections(this.currentConfig, newConfig);
|
|
286
255
|
if (changedSections.size > 0) {
|
|
287
256
|
const now = Date.now();
|
|
288
|
-
if (now - this.lastChangeAt >
|
|
257
|
+
if (now - this.lastChangeAt > this.config.cooldownMs) {
|
|
289
258
|
this.currentConfig = newConfig;
|
|
290
259
|
this.lastChangeAt = now;
|
|
291
260
|
const saveResult = this.storage.saveConfig(newConfig);
|
|
@@ -322,6 +291,57 @@ var ConfigTuner = class {
|
|
|
322
291
|
}
|
|
323
292
|
onConfigChange(callback) {
|
|
324
293
|
this.listeners.push(callback);
|
|
294
|
+
return () => {
|
|
295
|
+
const idx = this.listeners.indexOf(callback);
|
|
296
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
// Pure computation — derives the next config from aggregates and anomalies
|
|
300
|
+
// without mutating any state or applying cooldown checks.
|
|
301
|
+
computeNext(aggregates, anomalies) {
|
|
302
|
+
const next = {
|
|
303
|
+
circuitBreaker: { ...this.currentConfig.circuitBreaker },
|
|
304
|
+
bulkhead: { ...this.currentConfig.bulkhead },
|
|
305
|
+
httpClient: { ...this.currentConfig.httpClient }
|
|
306
|
+
};
|
|
307
|
+
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
308
|
+
const targetTimeout = Math.min(
|
|
309
|
+
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
310
|
+
this.config.maxTimeoutMs
|
|
311
|
+
);
|
|
312
|
+
if (Math.abs(targetTimeout - next.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
313
|
+
next.httpClient.timeoutMs = this.smoothValue(next.httpClient.timeoutMs, targetTimeout);
|
|
314
|
+
}
|
|
315
|
+
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
316
|
+
if (avgErrorRate > 0.1) {
|
|
317
|
+
next.httpClient.maxRetries = Math.min(next.httpClient.maxRetries + 1, 5);
|
|
318
|
+
} else if (avgErrorRate < 0.01 && next.httpClient.maxRetries > 1) {
|
|
319
|
+
next.httpClient.maxRetries = Math.max(next.httpClient.maxRetries - 1, 0);
|
|
320
|
+
}
|
|
321
|
+
const criticalAnomalies = anomalies.filter(
|
|
322
|
+
(a) => a.severity === "critical" || a.severity === "high"
|
|
323
|
+
).length;
|
|
324
|
+
if (criticalAnomalies > 0) {
|
|
325
|
+
next.circuitBreaker.failureThreshold = Math.max(
|
|
326
|
+
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
327
|
+
10
|
|
328
|
+
);
|
|
329
|
+
} else if (anomalies.length === 0) {
|
|
330
|
+
next.circuitBreaker.failureThreshold = Math.min(
|
|
331
|
+
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
332
|
+
80
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return next;
|
|
336
|
+
}
|
|
337
|
+
diffSections(prev, next) {
|
|
338
|
+
const changed = /* @__PURE__ */ new Set();
|
|
339
|
+
for (const key of Object.keys(next)) {
|
|
340
|
+
if (JSON.stringify(next[key]) !== JSON.stringify(prev[key])) {
|
|
341
|
+
changed.add(key);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return changed;
|
|
325
345
|
}
|
|
326
346
|
smoothValue(current, target) {
|
|
327
347
|
return current + (target - current) * this.config.smoothingFactor;
|
|
@@ -333,7 +353,7 @@ var DEFAULT_LOOP_CONFIG = {
|
|
|
333
353
|
defaultIntervalMs: 6e4,
|
|
334
354
|
windowSizeMinutes: 5,
|
|
335
355
|
minSamplesBeforeTuning: 10,
|
|
336
|
-
|
|
356
|
+
pruneTtlHours: 24
|
|
337
357
|
};
|
|
338
358
|
|
|
339
359
|
// src/core/feedback-loop/feedback-loop.ts
|
|
@@ -354,6 +374,7 @@ var FeedbackLoop = class {
|
|
|
354
374
|
storage;
|
|
355
375
|
observability;
|
|
356
376
|
timerId = null;
|
|
377
|
+
isProcessing = false;
|
|
357
378
|
config;
|
|
358
379
|
cycleListeners = [];
|
|
359
380
|
start(intervalMs) {
|
|
@@ -363,14 +384,20 @@ var FeedbackLoop = class {
|
|
|
363
384
|
}
|
|
364
385
|
const interval = intervalMs ?? this.config.defaultIntervalMs;
|
|
365
386
|
this.observability.info("Feedback loop started", { intervalMs: interval });
|
|
366
|
-
this.timerId = setInterval(() => {
|
|
367
|
-
this.
|
|
387
|
+
this.timerId = setInterval(async () => {
|
|
388
|
+
if (this.isProcessing) {
|
|
389
|
+
this.observability.warn("Skipping cycle: previous cycle still running");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
this.isProcessing = true;
|
|
393
|
+
try {
|
|
394
|
+
const result = await this.runOnce();
|
|
368
395
|
if (!result.ok) {
|
|
369
|
-
this.observability.error("Feedback loop cycle failed", {
|
|
370
|
-
error: result.error
|
|
371
|
-
});
|
|
396
|
+
this.observability.error("Feedback loop cycle failed", { error: result.error });
|
|
372
397
|
}
|
|
373
|
-
}
|
|
398
|
+
} finally {
|
|
399
|
+
this.isProcessing = false;
|
|
400
|
+
}
|
|
374
401
|
}, interval);
|
|
375
402
|
}
|
|
376
403
|
stop() {
|
|
@@ -388,11 +415,10 @@ var FeedbackLoop = class {
|
|
|
388
415
|
async runOnce() {
|
|
389
416
|
const cycleId = uuid2();
|
|
390
417
|
const startTime = Date.now();
|
|
418
|
+
const windowEnd = /* @__PURE__ */ new Date();
|
|
419
|
+
const windowStart = new Date(windowEnd.getTime() - this.config.windowSizeMinutes * 6e4);
|
|
391
420
|
this.observability.debug("Feedback cycle started", { cycleId });
|
|
392
|
-
const patternsResult = this.storage.getPatterns(
|
|
393
|
-
new Date(Date.now() - this.config.windowSizeMinutes * 6e4),
|
|
394
|
-
/* @__PURE__ */ new Date()
|
|
395
|
-
);
|
|
421
|
+
const patternsResult = this.storage.getPatterns(windowStart, windowEnd);
|
|
396
422
|
if (!patternsResult.ok) {
|
|
397
423
|
return fail4(storageError("Failed to collect patterns", patternsResult.error));
|
|
398
424
|
}
|
|
@@ -413,7 +439,8 @@ var FeedbackLoop = class {
|
|
|
413
439
|
return ok4(skippedEvent);
|
|
414
440
|
}
|
|
415
441
|
const aggregatesResult = this.patternRegistry.getAggregates(
|
|
416
|
-
this.config.windowSizeMinutes
|
|
442
|
+
this.config.windowSizeMinutes,
|
|
443
|
+
windowEnd
|
|
417
444
|
);
|
|
418
445
|
if (!aggregatesResult.ok) {
|
|
419
446
|
return fail4(aggregatesResult.error);
|
|
@@ -425,7 +452,10 @@ var FeedbackLoop = class {
|
|
|
425
452
|
}
|
|
426
453
|
const anomalies = anomaliesResult.value;
|
|
427
454
|
for (const anomaly of anomalies) {
|
|
428
|
-
this.storage.saveAnomaly(anomaly);
|
|
455
|
+
const saveAnomalyResult = this.storage.saveAnomaly(anomaly);
|
|
456
|
+
if (!saveAnomalyResult.ok) {
|
|
457
|
+
this.observability.warn("Failed to persist anomaly", { error: saveAnomalyResult.error });
|
|
458
|
+
}
|
|
429
459
|
}
|
|
430
460
|
if (anomalies.length > 0) {
|
|
431
461
|
this.observability.warn("Anomalies detected", {
|
|
@@ -434,12 +464,12 @@ var FeedbackLoop = class {
|
|
|
434
464
|
});
|
|
435
465
|
this.observability.incrementMetric("anomalies.detected", anomalies.length);
|
|
436
466
|
}
|
|
467
|
+
const previousConfig = this.configTuner.getCurrentConfig();
|
|
437
468
|
const tuneResult = this.configTuner.tune(aggregates, anomalies);
|
|
438
469
|
if (!tuneResult.ok) {
|
|
439
470
|
return fail4(tuneResult.error);
|
|
440
471
|
}
|
|
441
472
|
const newConfig = tuneResult.value;
|
|
442
|
-
const previousConfig = this.configTuner.getCurrentConfig();
|
|
443
473
|
const configChanges = {};
|
|
444
474
|
for (const key of Object.keys(newConfig)) {
|
|
445
475
|
if (JSON.stringify(newConfig[key]) !== JSON.stringify(previousConfig[key])) {
|
|
@@ -462,6 +492,11 @@ var FeedbackLoop = class {
|
|
|
462
492
|
for (const listener of this.cycleListeners) {
|
|
463
493
|
listener(cycleEvent);
|
|
464
494
|
}
|
|
495
|
+
const pruneCutoff = new Date(Date.now() - this.config.pruneTtlHours * 36e5);
|
|
496
|
+
const pruneResult = this.storage.prune(pruneCutoff);
|
|
497
|
+
if (pruneResult.ok && pruneResult.value > 0) {
|
|
498
|
+
this.observability.debug("Pruned old records", { count: pruneResult.value });
|
|
499
|
+
}
|
|
465
500
|
this.observability.info("Feedback cycle completed", {
|
|
466
501
|
cycleId,
|
|
467
502
|
patternsProcessed: cycleEvent.patternsProcessed,
|
|
@@ -474,6 +509,10 @@ var FeedbackLoop = class {
|
|
|
474
509
|
}
|
|
475
510
|
onCycle(callback) {
|
|
476
511
|
this.cycleListeners.push(callback);
|
|
512
|
+
return () => {
|
|
513
|
+
const idx = this.cycleListeners.indexOf(callback);
|
|
514
|
+
if (idx >= 0) this.cycleListeners.splice(idx, 1);
|
|
515
|
+
};
|
|
477
516
|
}
|
|
478
517
|
};
|
|
479
518
|
|
|
@@ -484,6 +523,11 @@ var DEFAULT_CONFIG2 = {
|
|
|
484
523
|
bulkhead: { maxConcurrentCalls: 10 },
|
|
485
524
|
httpClient: { timeoutMs: 1e4, maxRetries: 3 }
|
|
486
525
|
};
|
|
526
|
+
var DEFAULT_LIMITS = {
|
|
527
|
+
maxPatterns: 1e4,
|
|
528
|
+
maxAnomalies: 1e3,
|
|
529
|
+
maxCycles: 1e3
|
|
530
|
+
};
|
|
487
531
|
function percentile(sorted, p) {
|
|
488
532
|
if (sorted.length === 0) return 0;
|
|
489
533
|
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
@@ -494,9 +538,16 @@ var InMemoryStorage = class {
|
|
|
494
538
|
anomalies = [];
|
|
495
539
|
config = { ...DEFAULT_CONFIG2 };
|
|
496
540
|
cycles = [];
|
|
541
|
+
limits;
|
|
542
|
+
constructor(limits) {
|
|
543
|
+
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
|
544
|
+
}
|
|
497
545
|
savePattern(pattern) {
|
|
498
546
|
try {
|
|
499
547
|
this.patterns.push(pattern);
|
|
548
|
+
if (this.patterns.length > this.limits.maxPatterns) {
|
|
549
|
+
this.patterns.shift();
|
|
550
|
+
}
|
|
500
551
|
return ok5(void 0);
|
|
501
552
|
} catch (e) {
|
|
502
553
|
return fail5(storageError("Failed to save pattern", e));
|
|
@@ -513,26 +564,30 @@ var InMemoryStorage = class {
|
|
|
513
564
|
return fail5(storageError("Failed to get patterns", e));
|
|
514
565
|
}
|
|
515
566
|
}
|
|
516
|
-
getAggregates(windowMinutes) {
|
|
567
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
517
568
|
try {
|
|
518
|
-
const
|
|
519
|
-
const
|
|
569
|
+
const end = windowEnd ?? /* @__PURE__ */ new Date();
|
|
570
|
+
const cutoff = new Date(end.getTime() - windowMinutes * 6e4);
|
|
571
|
+
const recent = this.patterns.filter((p) => p.timestamp >= cutoff && p.timestamp <= end);
|
|
520
572
|
const groups = /* @__PURE__ */ new Map();
|
|
521
573
|
for (const p of recent) {
|
|
522
|
-
const key = `${p.method}
|
|
523
|
-
|
|
524
|
-
|
|
574
|
+
const key = `${p.method}\0${p.path}`;
|
|
575
|
+
let g = groups.get(key);
|
|
576
|
+
if (!g) {
|
|
577
|
+
g = { method: p.method, path: p.path, items: [] };
|
|
578
|
+
groups.set(key, g);
|
|
579
|
+
}
|
|
580
|
+
g.items.push(p);
|
|
525
581
|
}
|
|
526
582
|
const aggregates = [];
|
|
527
|
-
for (const
|
|
528
|
-
const [method, path] = key.split(":");
|
|
583
|
+
for (const { method, path, items } of groups.values()) {
|
|
529
584
|
const durations = items.map((i) => i.durationMs).sort((a, b) => a - b);
|
|
530
585
|
const errors = items.filter((i) => i.statusCode >= 500).length;
|
|
531
586
|
aggregates.push({
|
|
532
587
|
method,
|
|
533
588
|
path,
|
|
534
589
|
windowStart: cutoff,
|
|
535
|
-
windowEnd:
|
|
590
|
+
windowEnd: end,
|
|
536
591
|
count: items.length,
|
|
537
592
|
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
538
593
|
p50Ms: percentile(durations, 50),
|
|
@@ -550,6 +605,9 @@ var InMemoryStorage = class {
|
|
|
550
605
|
saveAnomaly(report) {
|
|
551
606
|
try {
|
|
552
607
|
this.anomalies.push(report);
|
|
608
|
+
if (this.anomalies.length > this.limits.maxAnomalies) {
|
|
609
|
+
this.anomalies.shift();
|
|
610
|
+
}
|
|
553
611
|
return ok5(void 0);
|
|
554
612
|
} catch (e) {
|
|
555
613
|
return fail5(storageError("Failed to save anomaly", e));
|
|
@@ -588,6 +646,9 @@ var InMemoryStorage = class {
|
|
|
588
646
|
saveCycleEvent(event) {
|
|
589
647
|
try {
|
|
590
648
|
this.cycles.push(event);
|
|
649
|
+
if (this.cycles.length > this.limits.maxCycles) {
|
|
650
|
+
this.cycles.shift();
|
|
651
|
+
}
|
|
591
652
|
return ok5(void 0);
|
|
592
653
|
} catch (e) {
|
|
593
654
|
return fail5(storageError("Failed to save cycle event", e));
|
|
@@ -676,10 +737,10 @@ var AutoLearningCore = class _AutoLearningCore {
|
|
|
676
737
|
return this.feedbackLoop.runOnce();
|
|
677
738
|
}
|
|
678
739
|
onConfigChange(callback) {
|
|
679
|
-
this.configTuner.onConfigChange(callback);
|
|
740
|
+
return this.configTuner.onConfigChange(callback);
|
|
680
741
|
}
|
|
681
742
|
onCycle(callback) {
|
|
682
|
-
this.feedbackLoop.onCycle(callback);
|
|
743
|
+
return this.feedbackLoop.onCycle(callback);
|
|
683
744
|
}
|
|
684
745
|
};
|
|
685
746
|
|
|
@@ -704,6 +765,7 @@ var AutoLearningInterceptor = class {
|
|
|
704
765
|
reflector;
|
|
705
766
|
core;
|
|
706
767
|
intercept(context, next) {
|
|
768
|
+
if (context.getType() !== "http") return next.handle();
|
|
707
769
|
const options = this.reflector.get(
|
|
708
770
|
AUTO_LEARN_METADATA,
|
|
709
771
|
context.getHandler()
|
|
@@ -714,26 +776,42 @@ var AutoLearningInterceptor = class {
|
|
|
714
776
|
const start = Date.now();
|
|
715
777
|
const req = context.switchToHttp().getRequest();
|
|
716
778
|
const { method, path } = this.extractRequestInfo(req);
|
|
779
|
+
const record = (statusCode) => {
|
|
780
|
+
const result = this.core.recordPattern({
|
|
781
|
+
method,
|
|
782
|
+
path,
|
|
783
|
+
statusCode,
|
|
784
|
+
durationMs: Date.now() - start,
|
|
785
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
786
|
+
metadata: this.buildMetadata(req, options)
|
|
787
|
+
});
|
|
788
|
+
if (!result.ok) {
|
|
789
|
+
this.core.observability.error("Failed to record pattern", { error: result.error });
|
|
790
|
+
}
|
|
791
|
+
};
|
|
717
792
|
return next.handle().pipe(
|
|
718
|
-
tap(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
statusCode: status,
|
|
725
|
-
durationMs: duration,
|
|
726
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
727
|
-
metadata: options.customMetadata ? options.customMetadata(req) : void 0
|
|
728
|
-
});
|
|
793
|
+
tap({
|
|
794
|
+
next: () => record(context.switchToHttp().getResponse().statusCode),
|
|
795
|
+
error: (err) => {
|
|
796
|
+
const status = typeof err?.getStatus === "function" ? err.getStatus() : err?.status ?? 500;
|
|
797
|
+
record(status);
|
|
798
|
+
}
|
|
729
799
|
})
|
|
730
800
|
);
|
|
731
801
|
}
|
|
732
802
|
extractRequestInfo(req) {
|
|
733
803
|
const method = req.method ?? "UNKNOWN";
|
|
734
|
-
const
|
|
804
|
+
const raw = req.route?.path ?? req.path ?? req.url ?? "/";
|
|
805
|
+
const path = raw.split("?")[0];
|
|
735
806
|
return { method, path };
|
|
736
807
|
}
|
|
808
|
+
buildMetadata(req, options) {
|
|
809
|
+
const meta = {};
|
|
810
|
+
if (options.trackParams && req.params) meta.params = req.params;
|
|
811
|
+
if (options.trackBody && req.body) meta.body = req.body;
|
|
812
|
+
if (options.customMetadata) Object.assign(meta, options.customMetadata(req));
|
|
813
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
814
|
+
}
|
|
737
815
|
};
|
|
738
816
|
AutoLearningInterceptor = __decorateClass([
|
|
739
817
|
Injectable(),
|
|
@@ -753,11 +831,23 @@ var AutoLearningAdaptersService = class {
|
|
|
753
831
|
moduleRef;
|
|
754
832
|
cbRegistry = null;
|
|
755
833
|
bhRegistry = null;
|
|
834
|
+
unsubConfigChange = null;
|
|
756
835
|
async onModuleInit() {
|
|
757
836
|
await this.resolveRegistries();
|
|
758
837
|
if (this.cbRegistry || this.bhRegistry) {
|
|
759
|
-
this.core.onConfigChange((config) => this.applyConfig(config));
|
|
838
|
+
this.unsubConfigChange = this.core.onConfigChange((config) => this.applyConfig(config));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
onApplicationBootstrap() {
|
|
842
|
+
if (this.options.autoStart !== false) {
|
|
843
|
+
this.core.startFeedbackLoop(this.options.intervalMs);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
onModuleDestroy() {
|
|
847
|
+
if (this.core.isFeedbackLoopRunning()) {
|
|
848
|
+
this.core.stopFeedbackLoop();
|
|
760
849
|
}
|
|
850
|
+
this.unsubConfigChange?.();
|
|
761
851
|
}
|
|
762
852
|
async resolveRegistries() {
|
|
763
853
|
if (this.options.adapters?.circuitBreaker) {
|