@backendkit-labs/auto-learning 0.1.3 → 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/README.md +17 -0
- 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.cjs
CHANGED
|
@@ -92,8 +92,8 @@ var PatternRegistry = class {
|
|
|
92
92
|
});
|
|
93
93
|
return (0, import_result.ok)(void 0);
|
|
94
94
|
}
|
|
95
|
-
getAggregates(windowMinutes) {
|
|
96
|
-
const result = this.storage.getAggregates(windowMinutes);
|
|
95
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
96
|
+
const result = this.storage.getAggregates(windowMinutes, windowEnd);
|
|
97
97
|
if (!result.ok) {
|
|
98
98
|
this.observability.error("Failed to get aggregates", { error: result.error });
|
|
99
99
|
return (0, import_result.fail)(storageError("Failed to get aggregates", result.error));
|
|
@@ -127,12 +127,18 @@ var PatternRegistry = class {
|
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
const uniqueEndpoints = new Set(all.map((p) => `${p.method}:${p.path}`));
|
|
130
|
-
|
|
130
|
+
let oldest = all[0].timestamp.getTime();
|
|
131
|
+
let newest = oldest;
|
|
132
|
+
for (const p of all) {
|
|
133
|
+
const t = p.timestamp.getTime();
|
|
134
|
+
if (t < oldest) oldest = t;
|
|
135
|
+
if (t > newest) newest = t;
|
|
136
|
+
}
|
|
131
137
|
return (0, import_result.ok)({
|
|
132
138
|
totalPatterns: all.length,
|
|
133
139
|
uniqueEndpoints: uniqueEndpoints.size,
|
|
134
|
-
oldestPattern: new Date(
|
|
135
|
-
newestPattern: new Date(
|
|
140
|
+
oldestPattern: new Date(oldest),
|
|
141
|
+
newestPattern: new Date(newest)
|
|
136
142
|
});
|
|
137
143
|
}
|
|
138
144
|
};
|
|
@@ -172,23 +178,20 @@ var AnomalyDetector = class {
|
|
|
172
178
|
});
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
|
-
if (current.statusCode >= 500 && baseline.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
detectedAt: /* @__PURE__ */ new Date()
|
|
188
|
-
});
|
|
189
|
-
}
|
|
181
|
+
if (current.statusCode >= 500 && baseline.errorRate < this.config.errorRateThreshold) {
|
|
182
|
+
reports.push({
|
|
183
|
+
id: (0, import_uuid.v4)(),
|
|
184
|
+
endpoint: current.path,
|
|
185
|
+
method: current.method,
|
|
186
|
+
severity: "high",
|
|
187
|
+
metric: "error_rate",
|
|
188
|
+
expectedValue: baseline.errorRate,
|
|
189
|
+
actualValue: 1,
|
|
190
|
+
deviation: 1 / Math.max(baseline.errorRate, 1e-3),
|
|
191
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
192
|
+
});
|
|
190
193
|
}
|
|
191
|
-
return (0, import_result2.ok)(reports
|
|
194
|
+
return (0, import_result2.ok)(reports);
|
|
192
195
|
} catch (e) {
|
|
193
196
|
return (0, import_result2.fail)(
|
|
194
197
|
anomalyDetectionFailed(
|
|
@@ -204,11 +207,13 @@ var AnomalyDetector = class {
|
|
|
204
207
|
baselineMap.set(`${b.method}:${b.path}`, b);
|
|
205
208
|
}
|
|
206
209
|
const reports = [];
|
|
210
|
+
const seenUnknown = /* @__PURE__ */ new Set();
|
|
207
211
|
for (const pattern of windowPatterns) {
|
|
208
212
|
const key = `${pattern.method}:${pattern.path}`;
|
|
209
213
|
const baseline = baselineMap.get(key);
|
|
210
214
|
if (!baseline) {
|
|
211
|
-
if (this.config.enableUnknownEndpointDetection) {
|
|
215
|
+
if (this.config.enableUnknownEndpointDetection && !seenUnknown.has(key)) {
|
|
216
|
+
seenUnknown.add(key);
|
|
212
217
|
reports.push({
|
|
213
218
|
id: (0, import_uuid.v4)(),
|
|
214
219
|
endpoint: pattern.path,
|
|
@@ -224,8 +229,8 @@ var AnomalyDetector = class {
|
|
|
224
229
|
continue;
|
|
225
230
|
}
|
|
226
231
|
const result = this.analyze(pattern, baseline);
|
|
227
|
-
if (result.ok
|
|
228
|
-
reports.push(result.value);
|
|
232
|
+
if (result.ok) {
|
|
233
|
+
reports.push(...result.value);
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
return (0, import_result2.ok)(reports);
|
|
@@ -253,7 +258,8 @@ var DEFAULT_TUNER_CONFIG = {
|
|
|
253
258
|
minTimeoutMs: 1e3,
|
|
254
259
|
maxTimeoutMs: 3e4,
|
|
255
260
|
smoothingFactor: 0.3,
|
|
256
|
-
adjustmentStepMs: 500
|
|
261
|
+
adjustmentStepMs: 500,
|
|
262
|
+
cooldownMs: 6e4
|
|
257
263
|
};
|
|
258
264
|
|
|
259
265
|
// src/core/config-tuner/config-tuner.ts
|
|
@@ -288,48 +294,11 @@ var ConfigTuner = class {
|
|
|
288
294
|
if (aggregates.length === 0) {
|
|
289
295
|
return (0, import_result3.ok)(this.getCurrentConfig());
|
|
290
296
|
}
|
|
291
|
-
const newConfig =
|
|
292
|
-
|
|
293
|
-
bulkhead: { ...this.currentConfig.bulkhead },
|
|
294
|
-
httpClient: { ...this.currentConfig.httpClient }
|
|
295
|
-
};
|
|
296
|
-
const changedSections = /* @__PURE__ */ new Set();
|
|
297
|
-
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
298
|
-
const targetTimeout = Math.min(
|
|
299
|
-
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
300
|
-
this.config.maxTimeoutMs
|
|
301
|
-
);
|
|
302
|
-
if (Math.abs(targetTimeout - newConfig.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
303
|
-
newConfig.httpClient.timeoutMs = this.smoothValue(newConfig.httpClient.timeoutMs, targetTimeout);
|
|
304
|
-
changedSections.add("httpClient");
|
|
305
|
-
}
|
|
306
|
-
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
307
|
-
if (avgErrorRate > 0.1) {
|
|
308
|
-
newConfig.httpClient.maxRetries = Math.min(newConfig.httpClient.maxRetries + 1, 5);
|
|
309
|
-
changedSections.add("httpClient");
|
|
310
|
-
} else if (avgErrorRate < 0.01 && newConfig.httpClient.maxRetries > 1) {
|
|
311
|
-
newConfig.httpClient.maxRetries = Math.max(newConfig.httpClient.maxRetries - 1, 0);
|
|
312
|
-
changedSections.add("httpClient");
|
|
313
|
-
}
|
|
314
|
-
const criticalAnomalies = anomalies.filter(
|
|
315
|
-
(a) => a.severity === "critical" || a.severity === "high"
|
|
316
|
-
).length;
|
|
317
|
-
if (criticalAnomalies > 0) {
|
|
318
|
-
newConfig.circuitBreaker.failureThreshold = Math.max(
|
|
319
|
-
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
320
|
-
10
|
|
321
|
-
);
|
|
322
|
-
changedSections.add("circuitBreaker");
|
|
323
|
-
} else if (anomalies.length === 0) {
|
|
324
|
-
newConfig.circuitBreaker.failureThreshold = Math.min(
|
|
325
|
-
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
326
|
-
80
|
|
327
|
-
);
|
|
328
|
-
changedSections.add("circuitBreaker");
|
|
329
|
-
}
|
|
297
|
+
const newConfig = this.computeNext(aggregates, anomalies);
|
|
298
|
+
const changedSections = this.diffSections(this.currentConfig, newConfig);
|
|
330
299
|
if (changedSections.size > 0) {
|
|
331
300
|
const now = Date.now();
|
|
332
|
-
if (now - this.lastChangeAt >
|
|
301
|
+
if (now - this.lastChangeAt > this.config.cooldownMs) {
|
|
333
302
|
this.currentConfig = newConfig;
|
|
334
303
|
this.lastChangeAt = now;
|
|
335
304
|
const saveResult = this.storage.saveConfig(newConfig);
|
|
@@ -366,6 +335,57 @@ var ConfigTuner = class {
|
|
|
366
335
|
}
|
|
367
336
|
onConfigChange(callback) {
|
|
368
337
|
this.listeners.push(callback);
|
|
338
|
+
return () => {
|
|
339
|
+
const idx = this.listeners.indexOf(callback);
|
|
340
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
// Pure computation — derives the next config from aggregates and anomalies
|
|
344
|
+
// without mutating any state or applying cooldown checks.
|
|
345
|
+
computeNext(aggregates, anomalies) {
|
|
346
|
+
const next = {
|
|
347
|
+
circuitBreaker: { ...this.currentConfig.circuitBreaker },
|
|
348
|
+
bulkhead: { ...this.currentConfig.bulkhead },
|
|
349
|
+
httpClient: { ...this.currentConfig.httpClient }
|
|
350
|
+
};
|
|
351
|
+
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
352
|
+
const targetTimeout = Math.min(
|
|
353
|
+
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
354
|
+
this.config.maxTimeoutMs
|
|
355
|
+
);
|
|
356
|
+
if (Math.abs(targetTimeout - next.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
357
|
+
next.httpClient.timeoutMs = this.smoothValue(next.httpClient.timeoutMs, targetTimeout);
|
|
358
|
+
}
|
|
359
|
+
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
360
|
+
if (avgErrorRate > 0.1) {
|
|
361
|
+
next.httpClient.maxRetries = Math.min(next.httpClient.maxRetries + 1, 5);
|
|
362
|
+
} else if (avgErrorRate < 0.01 && next.httpClient.maxRetries > 1) {
|
|
363
|
+
next.httpClient.maxRetries = Math.max(next.httpClient.maxRetries - 1, 0);
|
|
364
|
+
}
|
|
365
|
+
const criticalAnomalies = anomalies.filter(
|
|
366
|
+
(a) => a.severity === "critical" || a.severity === "high"
|
|
367
|
+
).length;
|
|
368
|
+
if (criticalAnomalies > 0) {
|
|
369
|
+
next.circuitBreaker.failureThreshold = Math.max(
|
|
370
|
+
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
371
|
+
10
|
|
372
|
+
);
|
|
373
|
+
} else if (anomalies.length === 0) {
|
|
374
|
+
next.circuitBreaker.failureThreshold = Math.min(
|
|
375
|
+
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
376
|
+
80
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return next;
|
|
380
|
+
}
|
|
381
|
+
diffSections(prev, next) {
|
|
382
|
+
const changed = /* @__PURE__ */ new Set();
|
|
383
|
+
for (const key of Object.keys(next)) {
|
|
384
|
+
if (JSON.stringify(next[key]) !== JSON.stringify(prev[key])) {
|
|
385
|
+
changed.add(key);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return changed;
|
|
369
389
|
}
|
|
370
390
|
smoothValue(current, target) {
|
|
371
391
|
return current + (target - current) * this.config.smoothingFactor;
|
|
@@ -377,7 +397,7 @@ var DEFAULT_LOOP_CONFIG = {
|
|
|
377
397
|
defaultIntervalMs: 6e4,
|
|
378
398
|
windowSizeMinutes: 5,
|
|
379
399
|
minSamplesBeforeTuning: 10,
|
|
380
|
-
|
|
400
|
+
pruneTtlHours: 24
|
|
381
401
|
};
|
|
382
402
|
|
|
383
403
|
// src/core/feedback-loop/feedback-loop.ts
|
|
@@ -398,6 +418,7 @@ var FeedbackLoop = class {
|
|
|
398
418
|
storage;
|
|
399
419
|
observability;
|
|
400
420
|
timerId = null;
|
|
421
|
+
isProcessing = false;
|
|
401
422
|
config;
|
|
402
423
|
cycleListeners = [];
|
|
403
424
|
start(intervalMs) {
|
|
@@ -407,14 +428,20 @@ var FeedbackLoop = class {
|
|
|
407
428
|
}
|
|
408
429
|
const interval = intervalMs ?? this.config.defaultIntervalMs;
|
|
409
430
|
this.observability.info("Feedback loop started", { intervalMs: interval });
|
|
410
|
-
this.timerId = setInterval(() => {
|
|
411
|
-
this.
|
|
431
|
+
this.timerId = setInterval(async () => {
|
|
432
|
+
if (this.isProcessing) {
|
|
433
|
+
this.observability.warn("Skipping cycle: previous cycle still running");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this.isProcessing = true;
|
|
437
|
+
try {
|
|
438
|
+
const result = await this.runOnce();
|
|
412
439
|
if (!result.ok) {
|
|
413
|
-
this.observability.error("Feedback loop cycle failed", {
|
|
414
|
-
error: result.error
|
|
415
|
-
});
|
|
440
|
+
this.observability.error("Feedback loop cycle failed", { error: result.error });
|
|
416
441
|
}
|
|
417
|
-
}
|
|
442
|
+
} finally {
|
|
443
|
+
this.isProcessing = false;
|
|
444
|
+
}
|
|
418
445
|
}, interval);
|
|
419
446
|
}
|
|
420
447
|
stop() {
|
|
@@ -432,11 +459,10 @@ var FeedbackLoop = class {
|
|
|
432
459
|
async runOnce() {
|
|
433
460
|
const cycleId = (0, import_uuid2.v4)();
|
|
434
461
|
const startTime = Date.now();
|
|
462
|
+
const windowEnd = /* @__PURE__ */ new Date();
|
|
463
|
+
const windowStart = new Date(windowEnd.getTime() - this.config.windowSizeMinutes * 6e4);
|
|
435
464
|
this.observability.debug("Feedback cycle started", { cycleId });
|
|
436
|
-
const patternsResult = this.storage.getPatterns(
|
|
437
|
-
new Date(Date.now() - this.config.windowSizeMinutes * 6e4),
|
|
438
|
-
/* @__PURE__ */ new Date()
|
|
439
|
-
);
|
|
465
|
+
const patternsResult = this.storage.getPatterns(windowStart, windowEnd);
|
|
440
466
|
if (!patternsResult.ok) {
|
|
441
467
|
return (0, import_result4.fail)(storageError("Failed to collect patterns", patternsResult.error));
|
|
442
468
|
}
|
|
@@ -457,7 +483,8 @@ var FeedbackLoop = class {
|
|
|
457
483
|
return (0, import_result4.ok)(skippedEvent);
|
|
458
484
|
}
|
|
459
485
|
const aggregatesResult = this.patternRegistry.getAggregates(
|
|
460
|
-
this.config.windowSizeMinutes
|
|
486
|
+
this.config.windowSizeMinutes,
|
|
487
|
+
windowEnd
|
|
461
488
|
);
|
|
462
489
|
if (!aggregatesResult.ok) {
|
|
463
490
|
return (0, import_result4.fail)(aggregatesResult.error);
|
|
@@ -469,7 +496,10 @@ var FeedbackLoop = class {
|
|
|
469
496
|
}
|
|
470
497
|
const anomalies = anomaliesResult.value;
|
|
471
498
|
for (const anomaly of anomalies) {
|
|
472
|
-
this.storage.saveAnomaly(anomaly);
|
|
499
|
+
const saveAnomalyResult = this.storage.saveAnomaly(anomaly);
|
|
500
|
+
if (!saveAnomalyResult.ok) {
|
|
501
|
+
this.observability.warn("Failed to persist anomaly", { error: saveAnomalyResult.error });
|
|
502
|
+
}
|
|
473
503
|
}
|
|
474
504
|
if (anomalies.length > 0) {
|
|
475
505
|
this.observability.warn("Anomalies detected", {
|
|
@@ -478,12 +508,12 @@ var FeedbackLoop = class {
|
|
|
478
508
|
});
|
|
479
509
|
this.observability.incrementMetric("anomalies.detected", anomalies.length);
|
|
480
510
|
}
|
|
511
|
+
const previousConfig = this.configTuner.getCurrentConfig();
|
|
481
512
|
const tuneResult = this.configTuner.tune(aggregates, anomalies);
|
|
482
513
|
if (!tuneResult.ok) {
|
|
483
514
|
return (0, import_result4.fail)(tuneResult.error);
|
|
484
515
|
}
|
|
485
516
|
const newConfig = tuneResult.value;
|
|
486
|
-
const previousConfig = this.configTuner.getCurrentConfig();
|
|
487
517
|
const configChanges = {};
|
|
488
518
|
for (const key of Object.keys(newConfig)) {
|
|
489
519
|
if (JSON.stringify(newConfig[key]) !== JSON.stringify(previousConfig[key])) {
|
|
@@ -506,6 +536,11 @@ var FeedbackLoop = class {
|
|
|
506
536
|
for (const listener of this.cycleListeners) {
|
|
507
537
|
listener(cycleEvent);
|
|
508
538
|
}
|
|
539
|
+
const pruneCutoff = new Date(Date.now() - this.config.pruneTtlHours * 36e5);
|
|
540
|
+
const pruneResult = this.storage.prune(pruneCutoff);
|
|
541
|
+
if (pruneResult.ok && pruneResult.value > 0) {
|
|
542
|
+
this.observability.debug("Pruned old records", { count: pruneResult.value });
|
|
543
|
+
}
|
|
509
544
|
this.observability.info("Feedback cycle completed", {
|
|
510
545
|
cycleId,
|
|
511
546
|
patternsProcessed: cycleEvent.patternsProcessed,
|
|
@@ -518,6 +553,10 @@ var FeedbackLoop = class {
|
|
|
518
553
|
}
|
|
519
554
|
onCycle(callback) {
|
|
520
555
|
this.cycleListeners.push(callback);
|
|
556
|
+
return () => {
|
|
557
|
+
const idx = this.cycleListeners.indexOf(callback);
|
|
558
|
+
if (idx >= 0) this.cycleListeners.splice(idx, 1);
|
|
559
|
+
};
|
|
521
560
|
}
|
|
522
561
|
};
|
|
523
562
|
|
|
@@ -528,6 +567,11 @@ var DEFAULT_CONFIG2 = {
|
|
|
528
567
|
bulkhead: { maxConcurrentCalls: 10 },
|
|
529
568
|
httpClient: { timeoutMs: 1e4, maxRetries: 3 }
|
|
530
569
|
};
|
|
570
|
+
var DEFAULT_LIMITS = {
|
|
571
|
+
maxPatterns: 1e4,
|
|
572
|
+
maxAnomalies: 1e3,
|
|
573
|
+
maxCycles: 1e3
|
|
574
|
+
};
|
|
531
575
|
function percentile(sorted, p) {
|
|
532
576
|
if (sorted.length === 0) return 0;
|
|
533
577
|
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
@@ -538,9 +582,16 @@ var InMemoryStorage = class {
|
|
|
538
582
|
anomalies = [];
|
|
539
583
|
config = { ...DEFAULT_CONFIG2 };
|
|
540
584
|
cycles = [];
|
|
585
|
+
limits;
|
|
586
|
+
constructor(limits) {
|
|
587
|
+
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
|
588
|
+
}
|
|
541
589
|
savePattern(pattern) {
|
|
542
590
|
try {
|
|
543
591
|
this.patterns.push(pattern);
|
|
592
|
+
if (this.patterns.length > this.limits.maxPatterns) {
|
|
593
|
+
this.patterns.shift();
|
|
594
|
+
}
|
|
544
595
|
return (0, import_result5.ok)(void 0);
|
|
545
596
|
} catch (e) {
|
|
546
597
|
return (0, import_result5.fail)(storageError("Failed to save pattern", e));
|
|
@@ -557,26 +608,30 @@ var InMemoryStorage = class {
|
|
|
557
608
|
return (0, import_result5.fail)(storageError("Failed to get patterns", e));
|
|
558
609
|
}
|
|
559
610
|
}
|
|
560
|
-
getAggregates(windowMinutes) {
|
|
611
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
561
612
|
try {
|
|
562
|
-
const
|
|
563
|
-
const
|
|
613
|
+
const end = windowEnd ?? /* @__PURE__ */ new Date();
|
|
614
|
+
const cutoff = new Date(end.getTime() - windowMinutes * 6e4);
|
|
615
|
+
const recent = this.patterns.filter((p) => p.timestamp >= cutoff && p.timestamp <= end);
|
|
564
616
|
const groups = /* @__PURE__ */ new Map();
|
|
565
617
|
for (const p of recent) {
|
|
566
|
-
const key = `${p.method}
|
|
567
|
-
|
|
568
|
-
|
|
618
|
+
const key = `${p.method}\0${p.path}`;
|
|
619
|
+
let g = groups.get(key);
|
|
620
|
+
if (!g) {
|
|
621
|
+
g = { method: p.method, path: p.path, items: [] };
|
|
622
|
+
groups.set(key, g);
|
|
623
|
+
}
|
|
624
|
+
g.items.push(p);
|
|
569
625
|
}
|
|
570
626
|
const aggregates = [];
|
|
571
|
-
for (const
|
|
572
|
-
const [method, path] = key.split(":");
|
|
627
|
+
for (const { method, path, items } of groups.values()) {
|
|
573
628
|
const durations = items.map((i) => i.durationMs).sort((a, b) => a - b);
|
|
574
629
|
const errors = items.filter((i) => i.statusCode >= 500).length;
|
|
575
630
|
aggregates.push({
|
|
576
631
|
method,
|
|
577
632
|
path,
|
|
578
633
|
windowStart: cutoff,
|
|
579
|
-
windowEnd:
|
|
634
|
+
windowEnd: end,
|
|
580
635
|
count: items.length,
|
|
581
636
|
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
582
637
|
p50Ms: percentile(durations, 50),
|
|
@@ -594,6 +649,9 @@ var InMemoryStorage = class {
|
|
|
594
649
|
saveAnomaly(report) {
|
|
595
650
|
try {
|
|
596
651
|
this.anomalies.push(report);
|
|
652
|
+
if (this.anomalies.length > this.limits.maxAnomalies) {
|
|
653
|
+
this.anomalies.shift();
|
|
654
|
+
}
|
|
597
655
|
return (0, import_result5.ok)(void 0);
|
|
598
656
|
} catch (e) {
|
|
599
657
|
return (0, import_result5.fail)(storageError("Failed to save anomaly", e));
|
|
@@ -632,6 +690,9 @@ var InMemoryStorage = class {
|
|
|
632
690
|
saveCycleEvent(event) {
|
|
633
691
|
try {
|
|
634
692
|
this.cycles.push(event);
|
|
693
|
+
if (this.cycles.length > this.limits.maxCycles) {
|
|
694
|
+
this.cycles.shift();
|
|
695
|
+
}
|
|
635
696
|
return (0, import_result5.ok)(void 0);
|
|
636
697
|
} catch (e) {
|
|
637
698
|
return (0, import_result5.fail)(storageError("Failed to save cycle event", e));
|
|
@@ -754,10 +815,10 @@ var AutoLearningCore = class _AutoLearningCore {
|
|
|
754
815
|
return this.feedbackLoop.runOnce();
|
|
755
816
|
}
|
|
756
817
|
onConfigChange(callback) {
|
|
757
|
-
this.configTuner.onConfigChange(callback);
|
|
818
|
+
return this.configTuner.onConfigChange(callback);
|
|
758
819
|
}
|
|
759
820
|
onCycle(callback) {
|
|
760
|
-
this.feedbackLoop.onCycle(callback);
|
|
821
|
+
return this.feedbackLoop.onCycle(callback);
|
|
761
822
|
}
|
|
762
823
|
};
|
|
763
824
|
|
|
@@ -783,6 +844,7 @@ var AutoLearningInterceptor = class {
|
|
|
783
844
|
reflector;
|
|
784
845
|
core;
|
|
785
846
|
intercept(context, next) {
|
|
847
|
+
if (context.getType() !== "http") return next.handle();
|
|
786
848
|
const options = this.reflector.get(
|
|
787
849
|
AUTO_LEARN_METADATA,
|
|
788
850
|
context.getHandler()
|
|
@@ -793,26 +855,42 @@ var AutoLearningInterceptor = class {
|
|
|
793
855
|
const start = Date.now();
|
|
794
856
|
const req = context.switchToHttp().getRequest();
|
|
795
857
|
const { method, path } = this.extractRequestInfo(req);
|
|
858
|
+
const record = (statusCode) => {
|
|
859
|
+
const result = this.core.recordPattern({
|
|
860
|
+
method,
|
|
861
|
+
path,
|
|
862
|
+
statusCode,
|
|
863
|
+
durationMs: Date.now() - start,
|
|
864
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
865
|
+
metadata: this.buildMetadata(req, options)
|
|
866
|
+
});
|
|
867
|
+
if (!result.ok) {
|
|
868
|
+
this.core.observability.error("Failed to record pattern", { error: result.error });
|
|
869
|
+
}
|
|
870
|
+
};
|
|
796
871
|
return next.handle().pipe(
|
|
797
|
-
(0, import_rxjs.tap)(
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
statusCode: status,
|
|
804
|
-
durationMs: duration,
|
|
805
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
806
|
-
metadata: options.customMetadata ? options.customMetadata(req) : void 0
|
|
807
|
-
});
|
|
872
|
+
(0, import_rxjs.tap)({
|
|
873
|
+
next: () => record(context.switchToHttp().getResponse().statusCode),
|
|
874
|
+
error: (err) => {
|
|
875
|
+
const status = typeof err?.getStatus === "function" ? err.getStatus() : err?.status ?? 500;
|
|
876
|
+
record(status);
|
|
877
|
+
}
|
|
808
878
|
})
|
|
809
879
|
);
|
|
810
880
|
}
|
|
811
881
|
extractRequestInfo(req) {
|
|
812
882
|
const method = req.method ?? "UNKNOWN";
|
|
813
|
-
const
|
|
883
|
+
const raw = req.route?.path ?? req.path ?? req.url ?? "/";
|
|
884
|
+
const path = raw.split("?")[0];
|
|
814
885
|
return { method, path };
|
|
815
886
|
}
|
|
887
|
+
buildMetadata(req, options) {
|
|
888
|
+
const meta = {};
|
|
889
|
+
if (options.trackParams && req.params) meta.params = req.params;
|
|
890
|
+
if (options.trackBody && req.body) meta.body = req.body;
|
|
891
|
+
if (options.customMetadata) Object.assign(meta, options.customMetadata(req));
|
|
892
|
+
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
893
|
+
}
|
|
816
894
|
};
|
|
817
895
|
AutoLearningInterceptor = __decorateClass([
|
|
818
896
|
(0, import_common.Injectable)(),
|
|
@@ -832,11 +910,23 @@ var AutoLearningAdaptersService = class {
|
|
|
832
910
|
moduleRef;
|
|
833
911
|
cbRegistry = null;
|
|
834
912
|
bhRegistry = null;
|
|
913
|
+
unsubConfigChange = null;
|
|
835
914
|
async onModuleInit() {
|
|
836
915
|
await this.resolveRegistries();
|
|
837
916
|
if (this.cbRegistry || this.bhRegistry) {
|
|
838
|
-
this.core.onConfigChange((config) => this.applyConfig(config));
|
|
917
|
+
this.unsubConfigChange = this.core.onConfigChange((config) => this.applyConfig(config));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
onApplicationBootstrap() {
|
|
921
|
+
if (this.options.autoStart !== false) {
|
|
922
|
+
this.core.startFeedbackLoop(this.options.intervalMs);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
onModuleDestroy() {
|
|
926
|
+
if (this.core.isFeedbackLoopRunning()) {
|
|
927
|
+
this.core.stopFeedbackLoop();
|
|
839
928
|
}
|
|
929
|
+
this.unsubConfigChange?.();
|
|
840
930
|
}
|
|
841
931
|
async resolveRegistries() {
|
|
842
932
|
if (this.options.adapters?.circuitBreaker) {
|