@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/index.js
CHANGED
|
@@ -44,8 +44,8 @@ var PatternRegistry = class {
|
|
|
44
44
|
});
|
|
45
45
|
return ok(void 0);
|
|
46
46
|
}
|
|
47
|
-
getAggregates(windowMinutes) {
|
|
48
|
-
const result = this.storage.getAggregates(windowMinutes);
|
|
47
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
48
|
+
const result = this.storage.getAggregates(windowMinutes, windowEnd);
|
|
49
49
|
if (!result.ok) {
|
|
50
50
|
this.observability.error("Failed to get aggregates", { error: result.error });
|
|
51
51
|
return fail(storageError("Failed to get aggregates", result.error));
|
|
@@ -79,12 +79,18 @@ var PatternRegistry = class {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
const uniqueEndpoints = new Set(all.map((p) => `${p.method}:${p.path}`));
|
|
82
|
-
|
|
82
|
+
let oldest = all[0].timestamp.getTime();
|
|
83
|
+
let newest = oldest;
|
|
84
|
+
for (const p of all) {
|
|
85
|
+
const t = p.timestamp.getTime();
|
|
86
|
+
if (t < oldest) oldest = t;
|
|
87
|
+
if (t > newest) newest = t;
|
|
88
|
+
}
|
|
83
89
|
return ok({
|
|
84
90
|
totalPatterns: all.length,
|
|
85
91
|
uniqueEndpoints: uniqueEndpoints.size,
|
|
86
|
-
oldestPattern: new Date(
|
|
87
|
-
newestPattern: new Date(
|
|
92
|
+
oldestPattern: new Date(oldest),
|
|
93
|
+
newestPattern: new Date(newest)
|
|
88
94
|
});
|
|
89
95
|
}
|
|
90
96
|
};
|
|
@@ -124,23 +130,20 @@ var AnomalyDetector = class {
|
|
|
124
130
|
});
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
|
-
if (current.statusCode >= 500 && baseline.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
detectedAt: /* @__PURE__ */ new Date()
|
|
140
|
-
});
|
|
141
|
-
}
|
|
133
|
+
if (current.statusCode >= 500 && baseline.errorRate < this.config.errorRateThreshold) {
|
|
134
|
+
reports.push({
|
|
135
|
+
id: uuid(),
|
|
136
|
+
endpoint: current.path,
|
|
137
|
+
method: current.method,
|
|
138
|
+
severity: "high",
|
|
139
|
+
metric: "error_rate",
|
|
140
|
+
expectedValue: baseline.errorRate,
|
|
141
|
+
actualValue: 1,
|
|
142
|
+
deviation: 1 / Math.max(baseline.errorRate, 1e-3),
|
|
143
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
144
|
+
});
|
|
142
145
|
}
|
|
143
|
-
return ok2(reports
|
|
146
|
+
return ok2(reports);
|
|
144
147
|
} catch (e) {
|
|
145
148
|
return fail2(
|
|
146
149
|
anomalyDetectionFailed(
|
|
@@ -156,11 +159,13 @@ var AnomalyDetector = class {
|
|
|
156
159
|
baselineMap.set(`${b.method}:${b.path}`, b);
|
|
157
160
|
}
|
|
158
161
|
const reports = [];
|
|
162
|
+
const seenUnknown = /* @__PURE__ */ new Set();
|
|
159
163
|
for (const pattern of windowPatterns) {
|
|
160
164
|
const key = `${pattern.method}:${pattern.path}`;
|
|
161
165
|
const baseline = baselineMap.get(key);
|
|
162
166
|
if (!baseline) {
|
|
163
|
-
if (this.config.enableUnknownEndpointDetection) {
|
|
167
|
+
if (this.config.enableUnknownEndpointDetection && !seenUnknown.has(key)) {
|
|
168
|
+
seenUnknown.add(key);
|
|
164
169
|
reports.push({
|
|
165
170
|
id: uuid(),
|
|
166
171
|
endpoint: pattern.path,
|
|
@@ -176,8 +181,8 @@ var AnomalyDetector = class {
|
|
|
176
181
|
continue;
|
|
177
182
|
}
|
|
178
183
|
const result = this.analyze(pattern, baseline);
|
|
179
|
-
if (result.ok
|
|
180
|
-
reports.push(result.value);
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
reports.push(...result.value);
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
return ok2(reports);
|
|
@@ -205,7 +210,8 @@ var DEFAULT_TUNER_CONFIG = {
|
|
|
205
210
|
minTimeoutMs: 1e3,
|
|
206
211
|
maxTimeoutMs: 3e4,
|
|
207
212
|
smoothingFactor: 0.3,
|
|
208
|
-
adjustmentStepMs: 500
|
|
213
|
+
adjustmentStepMs: 500,
|
|
214
|
+
cooldownMs: 6e4
|
|
209
215
|
};
|
|
210
216
|
|
|
211
217
|
// src/core/config-tuner/config-tuner.ts
|
|
@@ -240,48 +246,11 @@ var ConfigTuner = class {
|
|
|
240
246
|
if (aggregates.length === 0) {
|
|
241
247
|
return ok3(this.getCurrentConfig());
|
|
242
248
|
}
|
|
243
|
-
const newConfig =
|
|
244
|
-
|
|
245
|
-
bulkhead: { ...this.currentConfig.bulkhead },
|
|
246
|
-
httpClient: { ...this.currentConfig.httpClient }
|
|
247
|
-
};
|
|
248
|
-
const changedSections = /* @__PURE__ */ new Set();
|
|
249
|
-
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
250
|
-
const targetTimeout = Math.min(
|
|
251
|
-
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
252
|
-
this.config.maxTimeoutMs
|
|
253
|
-
);
|
|
254
|
-
if (Math.abs(targetTimeout - newConfig.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
255
|
-
newConfig.httpClient.timeoutMs = this.smoothValue(newConfig.httpClient.timeoutMs, targetTimeout);
|
|
256
|
-
changedSections.add("httpClient");
|
|
257
|
-
}
|
|
258
|
-
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
259
|
-
if (avgErrorRate > 0.1) {
|
|
260
|
-
newConfig.httpClient.maxRetries = Math.min(newConfig.httpClient.maxRetries + 1, 5);
|
|
261
|
-
changedSections.add("httpClient");
|
|
262
|
-
} else if (avgErrorRate < 0.01 && newConfig.httpClient.maxRetries > 1) {
|
|
263
|
-
newConfig.httpClient.maxRetries = Math.max(newConfig.httpClient.maxRetries - 1, 0);
|
|
264
|
-
changedSections.add("httpClient");
|
|
265
|
-
}
|
|
266
|
-
const criticalAnomalies = anomalies.filter(
|
|
267
|
-
(a) => a.severity === "critical" || a.severity === "high"
|
|
268
|
-
).length;
|
|
269
|
-
if (criticalAnomalies > 0) {
|
|
270
|
-
newConfig.circuitBreaker.failureThreshold = Math.max(
|
|
271
|
-
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
272
|
-
10
|
|
273
|
-
);
|
|
274
|
-
changedSections.add("circuitBreaker");
|
|
275
|
-
} else if (anomalies.length === 0) {
|
|
276
|
-
newConfig.circuitBreaker.failureThreshold = Math.min(
|
|
277
|
-
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
278
|
-
80
|
|
279
|
-
);
|
|
280
|
-
changedSections.add("circuitBreaker");
|
|
281
|
-
}
|
|
249
|
+
const newConfig = this.computeNext(aggregates, anomalies);
|
|
250
|
+
const changedSections = this.diffSections(this.currentConfig, newConfig);
|
|
282
251
|
if (changedSections.size > 0) {
|
|
283
252
|
const now = Date.now();
|
|
284
|
-
if (now - this.lastChangeAt >
|
|
253
|
+
if (now - this.lastChangeAt > this.config.cooldownMs) {
|
|
285
254
|
this.currentConfig = newConfig;
|
|
286
255
|
this.lastChangeAt = now;
|
|
287
256
|
const saveResult = this.storage.saveConfig(newConfig);
|
|
@@ -318,6 +287,57 @@ var ConfigTuner = class {
|
|
|
318
287
|
}
|
|
319
288
|
onConfigChange(callback) {
|
|
320
289
|
this.listeners.push(callback);
|
|
290
|
+
return () => {
|
|
291
|
+
const idx = this.listeners.indexOf(callback);
|
|
292
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// Pure computation — derives the next config from aggregates and anomalies
|
|
296
|
+
// without mutating any state or applying cooldown checks.
|
|
297
|
+
computeNext(aggregates, anomalies) {
|
|
298
|
+
const next = {
|
|
299
|
+
circuitBreaker: { ...this.currentConfig.circuitBreaker },
|
|
300
|
+
bulkhead: { ...this.currentConfig.bulkhead },
|
|
301
|
+
httpClient: { ...this.currentConfig.httpClient }
|
|
302
|
+
};
|
|
303
|
+
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
304
|
+
const targetTimeout = Math.min(
|
|
305
|
+
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
306
|
+
this.config.maxTimeoutMs
|
|
307
|
+
);
|
|
308
|
+
if (Math.abs(targetTimeout - next.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
309
|
+
next.httpClient.timeoutMs = this.smoothValue(next.httpClient.timeoutMs, targetTimeout);
|
|
310
|
+
}
|
|
311
|
+
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
312
|
+
if (avgErrorRate > 0.1) {
|
|
313
|
+
next.httpClient.maxRetries = Math.min(next.httpClient.maxRetries + 1, 5);
|
|
314
|
+
} else if (avgErrorRate < 0.01 && next.httpClient.maxRetries > 1) {
|
|
315
|
+
next.httpClient.maxRetries = Math.max(next.httpClient.maxRetries - 1, 0);
|
|
316
|
+
}
|
|
317
|
+
const criticalAnomalies = anomalies.filter(
|
|
318
|
+
(a) => a.severity === "critical" || a.severity === "high"
|
|
319
|
+
).length;
|
|
320
|
+
if (criticalAnomalies > 0) {
|
|
321
|
+
next.circuitBreaker.failureThreshold = Math.max(
|
|
322
|
+
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
323
|
+
10
|
|
324
|
+
);
|
|
325
|
+
} else if (anomalies.length === 0) {
|
|
326
|
+
next.circuitBreaker.failureThreshold = Math.min(
|
|
327
|
+
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
328
|
+
80
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return next;
|
|
332
|
+
}
|
|
333
|
+
diffSections(prev, next) {
|
|
334
|
+
const changed = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const key of Object.keys(next)) {
|
|
336
|
+
if (JSON.stringify(next[key]) !== JSON.stringify(prev[key])) {
|
|
337
|
+
changed.add(key);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return changed;
|
|
321
341
|
}
|
|
322
342
|
smoothValue(current, target) {
|
|
323
343
|
return current + (target - current) * this.config.smoothingFactor;
|
|
@@ -329,7 +349,7 @@ var DEFAULT_LOOP_CONFIG = {
|
|
|
329
349
|
defaultIntervalMs: 6e4,
|
|
330
350
|
windowSizeMinutes: 5,
|
|
331
351
|
minSamplesBeforeTuning: 10,
|
|
332
|
-
|
|
352
|
+
pruneTtlHours: 24
|
|
333
353
|
};
|
|
334
354
|
|
|
335
355
|
// src/core/feedback-loop/feedback-loop.ts
|
|
@@ -350,6 +370,7 @@ var FeedbackLoop = class {
|
|
|
350
370
|
storage;
|
|
351
371
|
observability;
|
|
352
372
|
timerId = null;
|
|
373
|
+
isProcessing = false;
|
|
353
374
|
config;
|
|
354
375
|
cycleListeners = [];
|
|
355
376
|
start(intervalMs) {
|
|
@@ -359,14 +380,20 @@ var FeedbackLoop = class {
|
|
|
359
380
|
}
|
|
360
381
|
const interval = intervalMs ?? this.config.defaultIntervalMs;
|
|
361
382
|
this.observability.info("Feedback loop started", { intervalMs: interval });
|
|
362
|
-
this.timerId = setInterval(() => {
|
|
363
|
-
this.
|
|
383
|
+
this.timerId = setInterval(async () => {
|
|
384
|
+
if (this.isProcessing) {
|
|
385
|
+
this.observability.warn("Skipping cycle: previous cycle still running");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.isProcessing = true;
|
|
389
|
+
try {
|
|
390
|
+
const result = await this.runOnce();
|
|
364
391
|
if (!result.ok) {
|
|
365
|
-
this.observability.error("Feedback loop cycle failed", {
|
|
366
|
-
error: result.error
|
|
367
|
-
});
|
|
392
|
+
this.observability.error("Feedback loop cycle failed", { error: result.error });
|
|
368
393
|
}
|
|
369
|
-
}
|
|
394
|
+
} finally {
|
|
395
|
+
this.isProcessing = false;
|
|
396
|
+
}
|
|
370
397
|
}, interval);
|
|
371
398
|
}
|
|
372
399
|
stop() {
|
|
@@ -384,11 +411,10 @@ var FeedbackLoop = class {
|
|
|
384
411
|
async runOnce() {
|
|
385
412
|
const cycleId = uuid2();
|
|
386
413
|
const startTime = Date.now();
|
|
414
|
+
const windowEnd = /* @__PURE__ */ new Date();
|
|
415
|
+
const windowStart = new Date(windowEnd.getTime() - this.config.windowSizeMinutes * 6e4);
|
|
387
416
|
this.observability.debug("Feedback cycle started", { cycleId });
|
|
388
|
-
const patternsResult = this.storage.getPatterns(
|
|
389
|
-
new Date(Date.now() - this.config.windowSizeMinutes * 6e4),
|
|
390
|
-
/* @__PURE__ */ new Date()
|
|
391
|
-
);
|
|
417
|
+
const patternsResult = this.storage.getPatterns(windowStart, windowEnd);
|
|
392
418
|
if (!patternsResult.ok) {
|
|
393
419
|
return fail4(storageError("Failed to collect patterns", patternsResult.error));
|
|
394
420
|
}
|
|
@@ -409,7 +435,8 @@ var FeedbackLoop = class {
|
|
|
409
435
|
return ok4(skippedEvent);
|
|
410
436
|
}
|
|
411
437
|
const aggregatesResult = this.patternRegistry.getAggregates(
|
|
412
|
-
this.config.windowSizeMinutes
|
|
438
|
+
this.config.windowSizeMinutes,
|
|
439
|
+
windowEnd
|
|
413
440
|
);
|
|
414
441
|
if (!aggregatesResult.ok) {
|
|
415
442
|
return fail4(aggregatesResult.error);
|
|
@@ -421,7 +448,10 @@ var FeedbackLoop = class {
|
|
|
421
448
|
}
|
|
422
449
|
const anomalies = anomaliesResult.value;
|
|
423
450
|
for (const anomaly of anomalies) {
|
|
424
|
-
this.storage.saveAnomaly(anomaly);
|
|
451
|
+
const saveAnomalyResult = this.storage.saveAnomaly(anomaly);
|
|
452
|
+
if (!saveAnomalyResult.ok) {
|
|
453
|
+
this.observability.warn("Failed to persist anomaly", { error: saveAnomalyResult.error });
|
|
454
|
+
}
|
|
425
455
|
}
|
|
426
456
|
if (anomalies.length > 0) {
|
|
427
457
|
this.observability.warn("Anomalies detected", {
|
|
@@ -430,12 +460,12 @@ var FeedbackLoop = class {
|
|
|
430
460
|
});
|
|
431
461
|
this.observability.incrementMetric("anomalies.detected", anomalies.length);
|
|
432
462
|
}
|
|
463
|
+
const previousConfig = this.configTuner.getCurrentConfig();
|
|
433
464
|
const tuneResult = this.configTuner.tune(aggregates, anomalies);
|
|
434
465
|
if (!tuneResult.ok) {
|
|
435
466
|
return fail4(tuneResult.error);
|
|
436
467
|
}
|
|
437
468
|
const newConfig = tuneResult.value;
|
|
438
|
-
const previousConfig = this.configTuner.getCurrentConfig();
|
|
439
469
|
const configChanges = {};
|
|
440
470
|
for (const key of Object.keys(newConfig)) {
|
|
441
471
|
if (JSON.stringify(newConfig[key]) !== JSON.stringify(previousConfig[key])) {
|
|
@@ -458,6 +488,11 @@ var FeedbackLoop = class {
|
|
|
458
488
|
for (const listener of this.cycleListeners) {
|
|
459
489
|
listener(cycleEvent);
|
|
460
490
|
}
|
|
491
|
+
const pruneCutoff = new Date(Date.now() - this.config.pruneTtlHours * 36e5);
|
|
492
|
+
const pruneResult = this.storage.prune(pruneCutoff);
|
|
493
|
+
if (pruneResult.ok && pruneResult.value > 0) {
|
|
494
|
+
this.observability.debug("Pruned old records", { count: pruneResult.value });
|
|
495
|
+
}
|
|
461
496
|
this.observability.info("Feedback cycle completed", {
|
|
462
497
|
cycleId,
|
|
463
498
|
patternsProcessed: cycleEvent.patternsProcessed,
|
|
@@ -470,6 +505,10 @@ var FeedbackLoop = class {
|
|
|
470
505
|
}
|
|
471
506
|
onCycle(callback) {
|
|
472
507
|
this.cycleListeners.push(callback);
|
|
508
|
+
return () => {
|
|
509
|
+
const idx = this.cycleListeners.indexOf(callback);
|
|
510
|
+
if (idx >= 0) this.cycleListeners.splice(idx, 1);
|
|
511
|
+
};
|
|
473
512
|
}
|
|
474
513
|
};
|
|
475
514
|
|
|
@@ -480,6 +519,11 @@ var DEFAULT_CONFIG2 = {
|
|
|
480
519
|
bulkhead: { maxConcurrentCalls: 10 },
|
|
481
520
|
httpClient: { timeoutMs: 1e4, maxRetries: 3 }
|
|
482
521
|
};
|
|
522
|
+
var DEFAULT_LIMITS = {
|
|
523
|
+
maxPatterns: 1e4,
|
|
524
|
+
maxAnomalies: 1e3,
|
|
525
|
+
maxCycles: 1e3
|
|
526
|
+
};
|
|
483
527
|
function percentile(sorted, p) {
|
|
484
528
|
if (sorted.length === 0) return 0;
|
|
485
529
|
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
@@ -490,9 +534,16 @@ var InMemoryStorage = class {
|
|
|
490
534
|
anomalies = [];
|
|
491
535
|
config = { ...DEFAULT_CONFIG2 };
|
|
492
536
|
cycles = [];
|
|
537
|
+
limits;
|
|
538
|
+
constructor(limits) {
|
|
539
|
+
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
|
540
|
+
}
|
|
493
541
|
savePattern(pattern) {
|
|
494
542
|
try {
|
|
495
543
|
this.patterns.push(pattern);
|
|
544
|
+
if (this.patterns.length > this.limits.maxPatterns) {
|
|
545
|
+
this.patterns.shift();
|
|
546
|
+
}
|
|
496
547
|
return ok5(void 0);
|
|
497
548
|
} catch (e) {
|
|
498
549
|
return fail5(storageError("Failed to save pattern", e));
|
|
@@ -509,26 +560,30 @@ var InMemoryStorage = class {
|
|
|
509
560
|
return fail5(storageError("Failed to get patterns", e));
|
|
510
561
|
}
|
|
511
562
|
}
|
|
512
|
-
getAggregates(windowMinutes) {
|
|
563
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
513
564
|
try {
|
|
514
|
-
const
|
|
515
|
-
const
|
|
565
|
+
const end = windowEnd ?? /* @__PURE__ */ new Date();
|
|
566
|
+
const cutoff = new Date(end.getTime() - windowMinutes * 6e4);
|
|
567
|
+
const recent = this.patterns.filter((p) => p.timestamp >= cutoff && p.timestamp <= end);
|
|
516
568
|
const groups = /* @__PURE__ */ new Map();
|
|
517
569
|
for (const p of recent) {
|
|
518
|
-
const key = `${p.method}
|
|
519
|
-
|
|
520
|
-
|
|
570
|
+
const key = `${p.method}\0${p.path}`;
|
|
571
|
+
let g = groups.get(key);
|
|
572
|
+
if (!g) {
|
|
573
|
+
g = { method: p.method, path: p.path, items: [] };
|
|
574
|
+
groups.set(key, g);
|
|
575
|
+
}
|
|
576
|
+
g.items.push(p);
|
|
521
577
|
}
|
|
522
578
|
const aggregates = [];
|
|
523
|
-
for (const
|
|
524
|
-
const [method, path] = key.split(":");
|
|
579
|
+
for (const { method, path, items } of groups.values()) {
|
|
525
580
|
const durations = items.map((i) => i.durationMs).sort((a, b) => a - b);
|
|
526
581
|
const errors = items.filter((i) => i.statusCode >= 500).length;
|
|
527
582
|
aggregates.push({
|
|
528
583
|
method,
|
|
529
584
|
path,
|
|
530
585
|
windowStart: cutoff,
|
|
531
|
-
windowEnd:
|
|
586
|
+
windowEnd: end,
|
|
532
587
|
count: items.length,
|
|
533
588
|
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
534
589
|
p50Ms: percentile(durations, 50),
|
|
@@ -546,6 +601,9 @@ var InMemoryStorage = class {
|
|
|
546
601
|
saveAnomaly(report) {
|
|
547
602
|
try {
|
|
548
603
|
this.anomalies.push(report);
|
|
604
|
+
if (this.anomalies.length > this.limits.maxAnomalies) {
|
|
605
|
+
this.anomalies.shift();
|
|
606
|
+
}
|
|
549
607
|
return ok5(void 0);
|
|
550
608
|
} catch (e) {
|
|
551
609
|
return fail5(storageError("Failed to save anomaly", e));
|
|
@@ -584,6 +642,9 @@ var InMemoryStorage = class {
|
|
|
584
642
|
saveCycleEvent(event) {
|
|
585
643
|
try {
|
|
586
644
|
this.cycles.push(event);
|
|
645
|
+
if (this.cycles.length > this.limits.maxCycles) {
|
|
646
|
+
this.cycles.shift();
|
|
647
|
+
}
|
|
587
648
|
return ok5(void 0);
|
|
588
649
|
} catch (e) {
|
|
589
650
|
return fail5(storageError("Failed to save cycle event", e));
|
|
@@ -706,10 +767,10 @@ var AutoLearningCore = class _AutoLearningCore {
|
|
|
706
767
|
return this.feedbackLoop.runOnce();
|
|
707
768
|
}
|
|
708
769
|
onConfigChange(callback) {
|
|
709
|
-
this.configTuner.onConfigChange(callback);
|
|
770
|
+
return this.configTuner.onConfigChange(callback);
|
|
710
771
|
}
|
|
711
772
|
onCycle(callback) {
|
|
712
|
-
this.feedbackLoop.onCycle(callback);
|
|
773
|
+
return this.feedbackLoop.onCycle(callback);
|
|
713
774
|
}
|
|
714
775
|
};
|
|
715
776
|
|
|
@@ -738,6 +799,7 @@ var AutoLearningInterceptor = class {
|
|
|
738
799
|
reflector;
|
|
739
800
|
core;
|
|
740
801
|
intercept(context, next) {
|
|
802
|
+
if (context.getType() !== "http") return next.handle();
|
|
741
803
|
const options = this.reflector.get(
|
|
742
804
|
AUTO_LEARN_METADATA,
|
|
743
805
|
context.getHandler()
|
|
@@ -748,26 +810,42 @@ var AutoLearningInterceptor = class {
|
|
|
748
810
|
const start = Date.now();
|
|
749
811
|
const req = context.switchToHttp().getRequest();
|
|
750
812
|
const { method, path } = this.extractRequestInfo(req);
|
|
813
|
+
const record = (statusCode) => {
|
|
814
|
+
const result = this.core.recordPattern({
|
|
815
|
+
method,
|
|
816
|
+
path,
|
|
817
|
+
statusCode,
|
|
818
|
+
durationMs: Date.now() - start,
|
|
819
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
820
|
+
metadata: this.buildMetadata(req, options)
|
|
821
|
+
});
|
|
822
|
+
if (!result.ok) {
|
|
823
|
+
this.core.observability.error("Failed to record pattern", { error: result.error });
|
|
824
|
+
}
|
|
825
|
+
};
|
|
751
826
|
return next.handle().pipe(
|
|
752
|
-
tap(
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
statusCode: status,
|
|
759
|
-
durationMs: duration,
|
|
760
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
761
|
-
metadata: options.customMetadata ? options.customMetadata(req) : void 0
|
|
762
|
-
});
|
|
827
|
+
tap({
|
|
828
|
+
next: () => record(context.switchToHttp().getResponse().statusCode),
|
|
829
|
+
error: (err) => {
|
|
830
|
+
const status = typeof err?.getStatus === "function" ? err.getStatus() : err?.status ?? 500;
|
|
831
|
+
record(status);
|
|
832
|
+
}
|
|
763
833
|
})
|
|
764
834
|
);
|
|
765
835
|
}
|
|
766
836
|
extractRequestInfo(req) {
|
|
767
837
|
const method = req.method ?? "UNKNOWN";
|
|
768
|
-
const
|
|
838
|
+
const raw = req.route?.path ?? req.path ?? req.url ?? "/";
|
|
839
|
+
const path = raw.split("?")[0];
|
|
769
840
|
return { method, path };
|
|
770
841
|
}
|
|
842
|
+
buildMetadata(req, options) {
|
|
843
|
+
const meta = {};
|
|
844
|
+
if (options.trackParams && req.params) meta.params = req.params;
|
|
845
|
+
if (options.trackBody && req.body) meta.body = req.body;
|
|
846
|
+
if (options.customMetadata) Object.assign(meta, options.customMetadata(req));
|
|
847
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
848
|
+
}
|
|
771
849
|
};
|
|
772
850
|
AutoLearningInterceptor = __decorateClass([
|
|
773
851
|
Injectable(),
|
|
@@ -787,11 +865,23 @@ var AutoLearningAdaptersService = class {
|
|
|
787
865
|
moduleRef;
|
|
788
866
|
cbRegistry = null;
|
|
789
867
|
bhRegistry = null;
|
|
868
|
+
unsubConfigChange = null;
|
|
790
869
|
async onModuleInit() {
|
|
791
870
|
await this.resolveRegistries();
|
|
792
871
|
if (this.cbRegistry || this.bhRegistry) {
|
|
793
|
-
this.core.onConfigChange((config) => this.applyConfig(config));
|
|
872
|
+
this.unsubConfigChange = this.core.onConfigChange((config) => this.applyConfig(config));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
onApplicationBootstrap() {
|
|
876
|
+
if (this.options.autoStart !== false) {
|
|
877
|
+
this.core.startFeedbackLoop(this.options.intervalMs);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
onModuleDestroy() {
|
|
881
|
+
if (this.core.isFeedbackLoopRunning()) {
|
|
882
|
+
this.core.stopFeedbackLoop();
|
|
794
883
|
}
|
|
884
|
+
this.unsubConfigChange?.();
|
|
795
885
|
}
|
|
796
886
|
async resolveRegistries() {
|
|
797
887
|
if (this.options.adapters?.circuitBreaker) {
|