@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/nestjs/index.cjs
CHANGED
|
@@ -85,8 +85,8 @@ var PatternRegistry = class {
|
|
|
85
85
|
});
|
|
86
86
|
return (0, import_result.ok)(void 0);
|
|
87
87
|
}
|
|
88
|
-
getAggregates(windowMinutes) {
|
|
89
|
-
const result = this.storage.getAggregates(windowMinutes);
|
|
88
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
89
|
+
const result = this.storage.getAggregates(windowMinutes, windowEnd);
|
|
90
90
|
if (!result.ok) {
|
|
91
91
|
this.observability.error("Failed to get aggregates", { error: result.error });
|
|
92
92
|
return (0, import_result.fail)(storageError("Failed to get aggregates", result.error));
|
|
@@ -120,12 +120,18 @@ var PatternRegistry = class {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
const uniqueEndpoints = new Set(all.map((p) => `${p.method}:${p.path}`));
|
|
123
|
-
|
|
123
|
+
let oldest = all[0].timestamp.getTime();
|
|
124
|
+
let newest = oldest;
|
|
125
|
+
for (const p of all) {
|
|
126
|
+
const t = p.timestamp.getTime();
|
|
127
|
+
if (t < oldest) oldest = t;
|
|
128
|
+
if (t > newest) newest = t;
|
|
129
|
+
}
|
|
124
130
|
return (0, import_result.ok)({
|
|
125
131
|
totalPatterns: all.length,
|
|
126
132
|
uniqueEndpoints: uniqueEndpoints.size,
|
|
127
|
-
oldestPattern: new Date(
|
|
128
|
-
newestPattern: new Date(
|
|
133
|
+
oldestPattern: new Date(oldest),
|
|
134
|
+
newestPattern: new Date(newest)
|
|
129
135
|
});
|
|
130
136
|
}
|
|
131
137
|
};
|
|
@@ -165,23 +171,20 @@ var AnomalyDetector = class {
|
|
|
165
171
|
});
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
|
-
if (current.statusCode >= 500 && baseline.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
detectedAt: /* @__PURE__ */ new Date()
|
|
181
|
-
});
|
|
182
|
-
}
|
|
174
|
+
if (current.statusCode >= 500 && baseline.errorRate < this.config.errorRateThreshold) {
|
|
175
|
+
reports.push({
|
|
176
|
+
id: (0, import_uuid.v4)(),
|
|
177
|
+
endpoint: current.path,
|
|
178
|
+
method: current.method,
|
|
179
|
+
severity: "high",
|
|
180
|
+
metric: "error_rate",
|
|
181
|
+
expectedValue: baseline.errorRate,
|
|
182
|
+
actualValue: 1,
|
|
183
|
+
deviation: 1 / Math.max(baseline.errorRate, 1e-3),
|
|
184
|
+
detectedAt: /* @__PURE__ */ new Date()
|
|
185
|
+
});
|
|
183
186
|
}
|
|
184
|
-
return (0, import_result2.ok)(reports
|
|
187
|
+
return (0, import_result2.ok)(reports);
|
|
185
188
|
} catch (e) {
|
|
186
189
|
return (0, import_result2.fail)(
|
|
187
190
|
anomalyDetectionFailed(
|
|
@@ -197,11 +200,13 @@ var AnomalyDetector = class {
|
|
|
197
200
|
baselineMap.set(`${b.method}:${b.path}`, b);
|
|
198
201
|
}
|
|
199
202
|
const reports = [];
|
|
203
|
+
const seenUnknown = /* @__PURE__ */ new Set();
|
|
200
204
|
for (const pattern of windowPatterns) {
|
|
201
205
|
const key = `${pattern.method}:${pattern.path}`;
|
|
202
206
|
const baseline = baselineMap.get(key);
|
|
203
207
|
if (!baseline) {
|
|
204
|
-
if (this.config.enableUnknownEndpointDetection) {
|
|
208
|
+
if (this.config.enableUnknownEndpointDetection && !seenUnknown.has(key)) {
|
|
209
|
+
seenUnknown.add(key);
|
|
205
210
|
reports.push({
|
|
206
211
|
id: (0, import_uuid.v4)(),
|
|
207
212
|
endpoint: pattern.path,
|
|
@@ -217,8 +222,8 @@ var AnomalyDetector = class {
|
|
|
217
222
|
continue;
|
|
218
223
|
}
|
|
219
224
|
const result = this.analyze(pattern, baseline);
|
|
220
|
-
if (result.ok
|
|
221
|
-
reports.push(result.value);
|
|
225
|
+
if (result.ok) {
|
|
226
|
+
reports.push(...result.value);
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
return (0, import_result2.ok)(reports);
|
|
@@ -246,7 +251,8 @@ var DEFAULT_TUNER_CONFIG = {
|
|
|
246
251
|
minTimeoutMs: 1e3,
|
|
247
252
|
maxTimeoutMs: 3e4,
|
|
248
253
|
smoothingFactor: 0.3,
|
|
249
|
-
adjustmentStepMs: 500
|
|
254
|
+
adjustmentStepMs: 500,
|
|
255
|
+
cooldownMs: 6e4
|
|
250
256
|
};
|
|
251
257
|
|
|
252
258
|
// src/core/config-tuner/config-tuner.ts
|
|
@@ -281,48 +287,11 @@ var ConfigTuner = class {
|
|
|
281
287
|
if (aggregates.length === 0) {
|
|
282
288
|
return (0, import_result3.ok)(this.getCurrentConfig());
|
|
283
289
|
}
|
|
284
|
-
const newConfig =
|
|
285
|
-
|
|
286
|
-
bulkhead: { ...this.currentConfig.bulkhead },
|
|
287
|
-
httpClient: { ...this.currentConfig.httpClient }
|
|
288
|
-
};
|
|
289
|
-
const changedSections = /* @__PURE__ */ new Set();
|
|
290
|
-
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
291
|
-
const targetTimeout = Math.min(
|
|
292
|
-
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
293
|
-
this.config.maxTimeoutMs
|
|
294
|
-
);
|
|
295
|
-
if (Math.abs(targetTimeout - newConfig.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
296
|
-
newConfig.httpClient.timeoutMs = this.smoothValue(newConfig.httpClient.timeoutMs, targetTimeout);
|
|
297
|
-
changedSections.add("httpClient");
|
|
298
|
-
}
|
|
299
|
-
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
300
|
-
if (avgErrorRate > 0.1) {
|
|
301
|
-
newConfig.httpClient.maxRetries = Math.min(newConfig.httpClient.maxRetries + 1, 5);
|
|
302
|
-
changedSections.add("httpClient");
|
|
303
|
-
} else if (avgErrorRate < 0.01 && newConfig.httpClient.maxRetries > 1) {
|
|
304
|
-
newConfig.httpClient.maxRetries = Math.max(newConfig.httpClient.maxRetries - 1, 0);
|
|
305
|
-
changedSections.add("httpClient");
|
|
306
|
-
}
|
|
307
|
-
const criticalAnomalies = anomalies.filter(
|
|
308
|
-
(a) => a.severity === "critical" || a.severity === "high"
|
|
309
|
-
).length;
|
|
310
|
-
if (criticalAnomalies > 0) {
|
|
311
|
-
newConfig.circuitBreaker.failureThreshold = Math.max(
|
|
312
|
-
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
313
|
-
10
|
|
314
|
-
);
|
|
315
|
-
changedSections.add("circuitBreaker");
|
|
316
|
-
} else if (anomalies.length === 0) {
|
|
317
|
-
newConfig.circuitBreaker.failureThreshold = Math.min(
|
|
318
|
-
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
319
|
-
80
|
|
320
|
-
);
|
|
321
|
-
changedSections.add("circuitBreaker");
|
|
322
|
-
}
|
|
290
|
+
const newConfig = this.computeNext(aggregates, anomalies);
|
|
291
|
+
const changedSections = this.diffSections(this.currentConfig, newConfig);
|
|
323
292
|
if (changedSections.size > 0) {
|
|
324
293
|
const now = Date.now();
|
|
325
|
-
if (now - this.lastChangeAt >
|
|
294
|
+
if (now - this.lastChangeAt > this.config.cooldownMs) {
|
|
326
295
|
this.currentConfig = newConfig;
|
|
327
296
|
this.lastChangeAt = now;
|
|
328
297
|
const saveResult = this.storage.saveConfig(newConfig);
|
|
@@ -359,6 +328,57 @@ var ConfigTuner = class {
|
|
|
359
328
|
}
|
|
360
329
|
onConfigChange(callback) {
|
|
361
330
|
this.listeners.push(callback);
|
|
331
|
+
return () => {
|
|
332
|
+
const idx = this.listeners.indexOf(callback);
|
|
333
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Pure computation — derives the next config from aggregates and anomalies
|
|
337
|
+
// without mutating any state or applying cooldown checks.
|
|
338
|
+
computeNext(aggregates, anomalies) {
|
|
339
|
+
const next = {
|
|
340
|
+
circuitBreaker: { ...this.currentConfig.circuitBreaker },
|
|
341
|
+
bulkhead: { ...this.currentConfig.bulkhead },
|
|
342
|
+
httpClient: { ...this.currentConfig.httpClient }
|
|
343
|
+
};
|
|
344
|
+
const maxP95 = Math.max(...aggregates.map((a) => a.p95Ms));
|
|
345
|
+
const targetTimeout = Math.min(
|
|
346
|
+
Math.max(maxP95 * 2, this.config.minTimeoutMs),
|
|
347
|
+
this.config.maxTimeoutMs
|
|
348
|
+
);
|
|
349
|
+
if (Math.abs(targetTimeout - next.httpClient.timeoutMs) > this.config.adjustmentStepMs) {
|
|
350
|
+
next.httpClient.timeoutMs = this.smoothValue(next.httpClient.timeoutMs, targetTimeout);
|
|
351
|
+
}
|
|
352
|
+
const avgErrorRate = aggregates.reduce((sum, a) => sum + a.errorRate, 0) / aggregates.length;
|
|
353
|
+
if (avgErrorRate > 0.1) {
|
|
354
|
+
next.httpClient.maxRetries = Math.min(next.httpClient.maxRetries + 1, 5);
|
|
355
|
+
} else if (avgErrorRate < 0.01 && next.httpClient.maxRetries > 1) {
|
|
356
|
+
next.httpClient.maxRetries = Math.max(next.httpClient.maxRetries - 1, 0);
|
|
357
|
+
}
|
|
358
|
+
const criticalAnomalies = anomalies.filter(
|
|
359
|
+
(a) => a.severity === "critical" || a.severity === "high"
|
|
360
|
+
).length;
|
|
361
|
+
if (criticalAnomalies > 0) {
|
|
362
|
+
next.circuitBreaker.failureThreshold = Math.max(
|
|
363
|
+
this.currentConfig.circuitBreaker.failureThreshold - 10 * criticalAnomalies,
|
|
364
|
+
10
|
|
365
|
+
);
|
|
366
|
+
} else if (anomalies.length === 0) {
|
|
367
|
+
next.circuitBreaker.failureThreshold = Math.min(
|
|
368
|
+
this.currentConfig.circuitBreaker.failureThreshold + 5,
|
|
369
|
+
80
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return next;
|
|
373
|
+
}
|
|
374
|
+
diffSections(prev, next) {
|
|
375
|
+
const changed = /* @__PURE__ */ new Set();
|
|
376
|
+
for (const key of Object.keys(next)) {
|
|
377
|
+
if (JSON.stringify(next[key]) !== JSON.stringify(prev[key])) {
|
|
378
|
+
changed.add(key);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return changed;
|
|
362
382
|
}
|
|
363
383
|
smoothValue(current, target) {
|
|
364
384
|
return current + (target - current) * this.config.smoothingFactor;
|
|
@@ -370,7 +390,7 @@ var DEFAULT_LOOP_CONFIG = {
|
|
|
370
390
|
defaultIntervalMs: 6e4,
|
|
371
391
|
windowSizeMinutes: 5,
|
|
372
392
|
minSamplesBeforeTuning: 10,
|
|
373
|
-
|
|
393
|
+
pruneTtlHours: 24
|
|
374
394
|
};
|
|
375
395
|
|
|
376
396
|
// src/core/feedback-loop/feedback-loop.ts
|
|
@@ -391,6 +411,7 @@ var FeedbackLoop = class {
|
|
|
391
411
|
storage;
|
|
392
412
|
observability;
|
|
393
413
|
timerId = null;
|
|
414
|
+
isProcessing = false;
|
|
394
415
|
config;
|
|
395
416
|
cycleListeners = [];
|
|
396
417
|
start(intervalMs) {
|
|
@@ -400,14 +421,20 @@ var FeedbackLoop = class {
|
|
|
400
421
|
}
|
|
401
422
|
const interval = intervalMs ?? this.config.defaultIntervalMs;
|
|
402
423
|
this.observability.info("Feedback loop started", { intervalMs: interval });
|
|
403
|
-
this.timerId = setInterval(() => {
|
|
404
|
-
this.
|
|
424
|
+
this.timerId = setInterval(async () => {
|
|
425
|
+
if (this.isProcessing) {
|
|
426
|
+
this.observability.warn("Skipping cycle: previous cycle still running");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.isProcessing = true;
|
|
430
|
+
try {
|
|
431
|
+
const result = await this.runOnce();
|
|
405
432
|
if (!result.ok) {
|
|
406
|
-
this.observability.error("Feedback loop cycle failed", {
|
|
407
|
-
error: result.error
|
|
408
|
-
});
|
|
433
|
+
this.observability.error("Feedback loop cycle failed", { error: result.error });
|
|
409
434
|
}
|
|
410
|
-
}
|
|
435
|
+
} finally {
|
|
436
|
+
this.isProcessing = false;
|
|
437
|
+
}
|
|
411
438
|
}, interval);
|
|
412
439
|
}
|
|
413
440
|
stop() {
|
|
@@ -425,11 +452,10 @@ var FeedbackLoop = class {
|
|
|
425
452
|
async runOnce() {
|
|
426
453
|
const cycleId = (0, import_uuid2.v4)();
|
|
427
454
|
const startTime = Date.now();
|
|
455
|
+
const windowEnd = /* @__PURE__ */ new Date();
|
|
456
|
+
const windowStart = new Date(windowEnd.getTime() - this.config.windowSizeMinutes * 6e4);
|
|
428
457
|
this.observability.debug("Feedback cycle started", { cycleId });
|
|
429
|
-
const patternsResult = this.storage.getPatterns(
|
|
430
|
-
new Date(Date.now() - this.config.windowSizeMinutes * 6e4),
|
|
431
|
-
/* @__PURE__ */ new Date()
|
|
432
|
-
);
|
|
458
|
+
const patternsResult = this.storage.getPatterns(windowStart, windowEnd);
|
|
433
459
|
if (!patternsResult.ok) {
|
|
434
460
|
return (0, import_result4.fail)(storageError("Failed to collect patterns", patternsResult.error));
|
|
435
461
|
}
|
|
@@ -450,7 +476,8 @@ var FeedbackLoop = class {
|
|
|
450
476
|
return (0, import_result4.ok)(skippedEvent);
|
|
451
477
|
}
|
|
452
478
|
const aggregatesResult = this.patternRegistry.getAggregates(
|
|
453
|
-
this.config.windowSizeMinutes
|
|
479
|
+
this.config.windowSizeMinutes,
|
|
480
|
+
windowEnd
|
|
454
481
|
);
|
|
455
482
|
if (!aggregatesResult.ok) {
|
|
456
483
|
return (0, import_result4.fail)(aggregatesResult.error);
|
|
@@ -462,7 +489,10 @@ var FeedbackLoop = class {
|
|
|
462
489
|
}
|
|
463
490
|
const anomalies = anomaliesResult.value;
|
|
464
491
|
for (const anomaly of anomalies) {
|
|
465
|
-
this.storage.saveAnomaly(anomaly);
|
|
492
|
+
const saveAnomalyResult = this.storage.saveAnomaly(anomaly);
|
|
493
|
+
if (!saveAnomalyResult.ok) {
|
|
494
|
+
this.observability.warn("Failed to persist anomaly", { error: saveAnomalyResult.error });
|
|
495
|
+
}
|
|
466
496
|
}
|
|
467
497
|
if (anomalies.length > 0) {
|
|
468
498
|
this.observability.warn("Anomalies detected", {
|
|
@@ -471,12 +501,12 @@ var FeedbackLoop = class {
|
|
|
471
501
|
});
|
|
472
502
|
this.observability.incrementMetric("anomalies.detected", anomalies.length);
|
|
473
503
|
}
|
|
504
|
+
const previousConfig = this.configTuner.getCurrentConfig();
|
|
474
505
|
const tuneResult = this.configTuner.tune(aggregates, anomalies);
|
|
475
506
|
if (!tuneResult.ok) {
|
|
476
507
|
return (0, import_result4.fail)(tuneResult.error);
|
|
477
508
|
}
|
|
478
509
|
const newConfig = tuneResult.value;
|
|
479
|
-
const previousConfig = this.configTuner.getCurrentConfig();
|
|
480
510
|
const configChanges = {};
|
|
481
511
|
for (const key of Object.keys(newConfig)) {
|
|
482
512
|
if (JSON.stringify(newConfig[key]) !== JSON.stringify(previousConfig[key])) {
|
|
@@ -499,6 +529,11 @@ var FeedbackLoop = class {
|
|
|
499
529
|
for (const listener of this.cycleListeners) {
|
|
500
530
|
listener(cycleEvent);
|
|
501
531
|
}
|
|
532
|
+
const pruneCutoff = new Date(Date.now() - this.config.pruneTtlHours * 36e5);
|
|
533
|
+
const pruneResult = this.storage.prune(pruneCutoff);
|
|
534
|
+
if (pruneResult.ok && pruneResult.value > 0) {
|
|
535
|
+
this.observability.debug("Pruned old records", { count: pruneResult.value });
|
|
536
|
+
}
|
|
502
537
|
this.observability.info("Feedback cycle completed", {
|
|
503
538
|
cycleId,
|
|
504
539
|
patternsProcessed: cycleEvent.patternsProcessed,
|
|
@@ -511,6 +546,10 @@ var FeedbackLoop = class {
|
|
|
511
546
|
}
|
|
512
547
|
onCycle(callback) {
|
|
513
548
|
this.cycleListeners.push(callback);
|
|
549
|
+
return () => {
|
|
550
|
+
const idx = this.cycleListeners.indexOf(callback);
|
|
551
|
+
if (idx >= 0) this.cycleListeners.splice(idx, 1);
|
|
552
|
+
};
|
|
514
553
|
}
|
|
515
554
|
};
|
|
516
555
|
|
|
@@ -521,6 +560,11 @@ var DEFAULT_CONFIG2 = {
|
|
|
521
560
|
bulkhead: { maxConcurrentCalls: 10 },
|
|
522
561
|
httpClient: { timeoutMs: 1e4, maxRetries: 3 }
|
|
523
562
|
};
|
|
563
|
+
var DEFAULT_LIMITS = {
|
|
564
|
+
maxPatterns: 1e4,
|
|
565
|
+
maxAnomalies: 1e3,
|
|
566
|
+
maxCycles: 1e3
|
|
567
|
+
};
|
|
524
568
|
function percentile(sorted, p) {
|
|
525
569
|
if (sorted.length === 0) return 0;
|
|
526
570
|
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
@@ -531,9 +575,16 @@ var InMemoryStorage = class {
|
|
|
531
575
|
anomalies = [];
|
|
532
576
|
config = { ...DEFAULT_CONFIG2 };
|
|
533
577
|
cycles = [];
|
|
578
|
+
limits;
|
|
579
|
+
constructor(limits) {
|
|
580
|
+
this.limits = { ...DEFAULT_LIMITS, ...limits };
|
|
581
|
+
}
|
|
534
582
|
savePattern(pattern) {
|
|
535
583
|
try {
|
|
536
584
|
this.patterns.push(pattern);
|
|
585
|
+
if (this.patterns.length > this.limits.maxPatterns) {
|
|
586
|
+
this.patterns.shift();
|
|
587
|
+
}
|
|
537
588
|
return (0, import_result5.ok)(void 0);
|
|
538
589
|
} catch (e) {
|
|
539
590
|
return (0, import_result5.fail)(storageError("Failed to save pattern", e));
|
|
@@ -550,26 +601,30 @@ var InMemoryStorage = class {
|
|
|
550
601
|
return (0, import_result5.fail)(storageError("Failed to get patterns", e));
|
|
551
602
|
}
|
|
552
603
|
}
|
|
553
|
-
getAggregates(windowMinutes) {
|
|
604
|
+
getAggregates(windowMinutes, windowEnd) {
|
|
554
605
|
try {
|
|
555
|
-
const
|
|
556
|
-
const
|
|
606
|
+
const end = windowEnd ?? /* @__PURE__ */ new Date();
|
|
607
|
+
const cutoff = new Date(end.getTime() - windowMinutes * 6e4);
|
|
608
|
+
const recent = this.patterns.filter((p) => p.timestamp >= cutoff && p.timestamp <= end);
|
|
557
609
|
const groups = /* @__PURE__ */ new Map();
|
|
558
610
|
for (const p of recent) {
|
|
559
|
-
const key = `${p.method}
|
|
560
|
-
|
|
561
|
-
|
|
611
|
+
const key = `${p.method}\0${p.path}`;
|
|
612
|
+
let g = groups.get(key);
|
|
613
|
+
if (!g) {
|
|
614
|
+
g = { method: p.method, path: p.path, items: [] };
|
|
615
|
+
groups.set(key, g);
|
|
616
|
+
}
|
|
617
|
+
g.items.push(p);
|
|
562
618
|
}
|
|
563
619
|
const aggregates = [];
|
|
564
|
-
for (const
|
|
565
|
-
const [method, path] = key.split(":");
|
|
620
|
+
for (const { method, path, items } of groups.values()) {
|
|
566
621
|
const durations = items.map((i) => i.durationMs).sort((a, b) => a - b);
|
|
567
622
|
const errors = items.filter((i) => i.statusCode >= 500).length;
|
|
568
623
|
aggregates.push({
|
|
569
624
|
method,
|
|
570
625
|
path,
|
|
571
626
|
windowStart: cutoff,
|
|
572
|
-
windowEnd:
|
|
627
|
+
windowEnd: end,
|
|
573
628
|
count: items.length,
|
|
574
629
|
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
575
630
|
p50Ms: percentile(durations, 50),
|
|
@@ -587,6 +642,9 @@ var InMemoryStorage = class {
|
|
|
587
642
|
saveAnomaly(report) {
|
|
588
643
|
try {
|
|
589
644
|
this.anomalies.push(report);
|
|
645
|
+
if (this.anomalies.length > this.limits.maxAnomalies) {
|
|
646
|
+
this.anomalies.shift();
|
|
647
|
+
}
|
|
590
648
|
return (0, import_result5.ok)(void 0);
|
|
591
649
|
} catch (e) {
|
|
592
650
|
return (0, import_result5.fail)(storageError("Failed to save anomaly", e));
|
|
@@ -625,6 +683,9 @@ var InMemoryStorage = class {
|
|
|
625
683
|
saveCycleEvent(event) {
|
|
626
684
|
try {
|
|
627
685
|
this.cycles.push(event);
|
|
686
|
+
if (this.cycles.length > this.limits.maxCycles) {
|
|
687
|
+
this.cycles.shift();
|
|
688
|
+
}
|
|
628
689
|
return (0, import_result5.ok)(void 0);
|
|
629
690
|
} catch (e) {
|
|
630
691
|
return (0, import_result5.fail)(storageError("Failed to save cycle event", e));
|
|
@@ -713,10 +774,10 @@ var AutoLearningCore = class _AutoLearningCore {
|
|
|
713
774
|
return this.feedbackLoop.runOnce();
|
|
714
775
|
}
|
|
715
776
|
onConfigChange(callback) {
|
|
716
|
-
this.configTuner.onConfigChange(callback);
|
|
777
|
+
return this.configTuner.onConfigChange(callback);
|
|
717
778
|
}
|
|
718
779
|
onCycle(callback) {
|
|
719
|
-
this.feedbackLoop.onCycle(callback);
|
|
780
|
+
return this.feedbackLoop.onCycle(callback);
|
|
720
781
|
}
|
|
721
782
|
};
|
|
722
783
|
|
|
@@ -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
|
-
(0, import_rxjs.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
|
+
(0, import_rxjs.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
|
(0, import_common.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) {
|