@lssm/lib.observability 0.0.0-canary-20251120170226 → 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/.turbo/turbo-build.log +92 -29
- package/.turbo/turbo-lint.log +20 -0
- package/CHANGELOG.md +13 -2
- package/README.md +7 -0
- package/dist/anomaly/alert-manager.d.mts +21 -0
- package/dist/anomaly/alert-manager.d.mts.map +1 -0
- package/dist/anomaly/alert-manager.mjs +2 -0
- package/dist/anomaly/alert-manager.mjs.map +1 -0
- package/dist/anomaly/anomaly-detector.d.mts +26 -0
- package/dist/anomaly/anomaly-detector.d.mts.map +1 -0
- package/dist/anomaly/anomaly-detector.mjs +2 -0
- package/dist/anomaly/anomaly-detector.mjs.map +1 -0
- package/dist/anomaly/baseline-calculator.d.mts +26 -0
- package/dist/anomaly/baseline-calculator.d.mts.map +1 -0
- package/dist/anomaly/baseline-calculator.mjs +2 -0
- package/dist/anomaly/baseline-calculator.mjs.map +1 -0
- package/dist/anomaly/root-cause-analyzer.d.mts +23 -0
- package/dist/anomaly/root-cause-analyzer.d.mts.map +1 -0
- package/dist/anomaly/root-cause-analyzer.mjs +2 -0
- package/dist/anomaly/root-cause-analyzer.mjs.map +1 -0
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +1 -1
- package/dist/intent/aggregator.d.mts +60 -0
- package/dist/intent/aggregator.d.mts.map +1 -0
- package/dist/intent/aggregator.mjs +2 -0
- package/dist/intent/aggregator.mjs.map +1 -0
- package/dist/intent/detector.d.mts +32 -0
- package/dist/intent/detector.d.mts.map +1 -0
- package/dist/intent/detector.mjs +2 -0
- package/dist/intent/detector.mjs.map +1 -0
- package/dist/lifecycle/dist/index.mjs +1 -0
- package/dist/lifecycle/dist/types/axes.mjs +2 -0
- package/dist/lifecycle/dist/types/axes.mjs.map +1 -0
- package/dist/lifecycle/dist/types/milestones.mjs +1 -0
- package/dist/lifecycle/dist/types/signals.mjs +1 -0
- package/dist/lifecycle/dist/types/stages.mjs +2 -0
- package/dist/lifecycle/dist/types/stages.mjs.map +1 -0
- package/dist/lifecycle/dist/utils/formatters.mjs +2 -0
- package/dist/lifecycle/dist/utils/formatters.mjs.map +1 -0
- package/dist/pipeline/evolution-pipeline.d.mts +40 -0
- package/dist/pipeline/evolution-pipeline.d.mts.map +1 -0
- package/dist/pipeline/evolution-pipeline.mjs +2 -0
- package/dist/pipeline/evolution-pipeline.mjs.map +1 -0
- package/dist/pipeline/lifecycle-pipeline.d.mts +44 -0
- package/dist/pipeline/lifecycle-pipeline.d.mts.map +1 -0
- package/dist/pipeline/lifecycle-pipeline.mjs +2 -0
- package/dist/pipeline/lifecycle-pipeline.mjs.map +1 -0
- package/dist/tracing/middleware.d.mts +16 -2
- package/dist/tracing/middleware.d.mts.map +1 -1
- package/dist/tracing/middleware.mjs +1 -1
- package/dist/tracing/middleware.mjs.map +1 -1
- package/package.json +12 -1
- package/src/anomaly/alert-manager.ts +31 -0
- package/src/anomaly/anomaly-detector.ts +94 -0
- package/src/anomaly/baseline-calculator.ts +54 -0
- package/src/anomaly/root-cause-analyzer.ts +55 -0
- package/src/index.ts +36 -1
- package/src/intent/aggregator.ts +161 -0
- package/src/intent/detector.ts +187 -0
- package/src/pipeline/evolution-pipeline.ts +90 -0
- package/src/pipeline/lifecycle-pipeline.ts +105 -0
- package/src/tracing/middleware.ts +77 -1
- package/tsconfig.json +7 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IntentAggregator, IntentAggregatorSnapshot, TelemetrySample } from "../intent/aggregator.mjs";
|
|
2
|
+
import { IntentDetector, IntentSignal } from "../intent/detector.mjs";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
|
|
5
|
+
//#region src/pipeline/evolution-pipeline.d.ts
|
|
6
|
+
type EvolutionPipelineEvent = {
|
|
7
|
+
type: 'intent.detected';
|
|
8
|
+
payload: IntentSignal;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'telemetry.window';
|
|
11
|
+
payload: {
|
|
12
|
+
sampleCount: number;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
interface EvolutionPipelineOptions {
|
|
16
|
+
detector?: IntentDetector;
|
|
17
|
+
aggregator?: IntentAggregator;
|
|
18
|
+
emitter?: EventEmitter;
|
|
19
|
+
onIntent?: (intent: IntentSignal) => Promise<void> | void;
|
|
20
|
+
onSnapshot?: (snapshot: IntentAggregatorSnapshot) => Promise<void> | void;
|
|
21
|
+
}
|
|
22
|
+
declare class EvolutionPipeline {
|
|
23
|
+
private readonly detector;
|
|
24
|
+
private readonly aggregator;
|
|
25
|
+
private readonly emitter;
|
|
26
|
+
private readonly onIntent?;
|
|
27
|
+
private readonly onSnapshot?;
|
|
28
|
+
private timer?;
|
|
29
|
+
private previousMetrics?;
|
|
30
|
+
constructor(options?: EvolutionPipelineOptions);
|
|
31
|
+
ingest(sample: TelemetrySample): void;
|
|
32
|
+
on(listener: (event: EvolutionPipelineEvent) => void): void;
|
|
33
|
+
start(intervalMs?: number): void;
|
|
34
|
+
stop(): void;
|
|
35
|
+
run(): Promise<void>;
|
|
36
|
+
private emit;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { EvolutionPipeline, EvolutionPipelineEvent, EvolutionPipelineOptions };
|
|
40
|
+
//# sourceMappingURL=evolution-pipeline.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evolution-pipeline.d.mts","names":[],"sources":["../../src/pipeline/evolution-pipeline.ts"],"sourcesContent":[],"mappings":";;;;;KAQY,sBAAA;;EAAA,OAAA,EAC4B,YAD5B;AAIZ,CAAA,GAAiB;EACJ,IAAA,EAAA,kBAAA;EACE,OAAA,EAAA;IACH,WAAA,EAAA,MAAA;EACU,CAAA;CAAiB;AACb,UALT,wBAAA,CAKS;EAA6B,QAAA,CAAA,EAJ1C,cAI0C;EAAO,UAAA,CAAA,EAH/C,gBAG+C;EAGjD,OAAA,CAAA,EALD,YAKkB;EAWP,QAAA,CAAA,EAAA,CAAA,MAAA,EAfD,YAeC,EAAA,GAfgB,OAehB,CAAA,IAAA,CAAA,GAAA,IAAA;EAQN,UAAA,CAAA,EAAA,CAAA,QAAA,EAtBS,wBAsBT,EAAA,GAtBsC,OAsBtC,CAAA,IAAA,CAAA,GAAA,IAAA;;AAsBN,cAzCE,iBAAA,CAyCF;EAAA,iBAAA,QAAA;;;;;;;wBA9BY;iBAQN;uBAIM;;;SAkBZ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{IntentAggregator as e}from"../intent/aggregator.mjs";import{IntentDetector as t}from"../intent/detector.mjs";import{EventEmitter as n}from"node:events";var r=class{detector;aggregator;emitter;onIntent;onSnapshot;timer;previousMetrics;constructor(r={}){this.detector=r.detector??new t,this.aggregator=r.aggregator??new e,this.emitter=r.emitter??new n,this.onIntent=r.onIntent,this.onSnapshot=r.onSnapshot}ingest(e){this.aggregator.add(e)}on(e){this.emitter.on(`event`,e)}start(e=300*1e3){this.stop(),this.timer=setInterval(()=>{this.run()},e)}stop(){this.timer&&=(clearInterval(this.timer),void 0)}async run(){let e=this.aggregator.flush();if(this.emit({type:`telemetry.window`,payload:{sampleCount:e.sampleCount}}),this.onSnapshot&&await this.onSnapshot(e),!e.sampleCount)return;let t=this.detector.detectFromMetrics(e.metrics,this.previousMetrics),n=this.detector.detectSequentialIntents(e.sequences);this.previousMetrics=e.metrics;let r=[...t,...n];for(let e of r)this.onIntent&&await this.onIntent(e),this.emit({type:`intent.detected`,payload:e})}emit(e){this.emitter.emit(`event`,e)}};export{r as EvolutionPipeline};
|
|
2
|
+
//# sourceMappingURL=evolution-pipeline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evolution-pipeline.mjs","names":[],"sources":["../../src/pipeline/evolution-pipeline.ts"],"sourcesContent":["import { EventEmitter } from 'node:events';\nimport {\n IntentAggregator,\n type IntentAggregatorSnapshot,\n type TelemetrySample,\n} from '../intent/aggregator';\nimport { IntentDetector, type IntentSignal } from '../intent/detector';\n\nexport type EvolutionPipelineEvent =\n | { type: 'intent.detected'; payload: IntentSignal }\n | { type: 'telemetry.window'; payload: { sampleCount: number } };\n\nexport interface EvolutionPipelineOptions {\n detector?: IntentDetector;\n aggregator?: IntentAggregator;\n emitter?: EventEmitter;\n onIntent?: (intent: IntentSignal) => Promise<void> | void;\n onSnapshot?: (snapshot: IntentAggregatorSnapshot) => Promise<void> | void;\n}\n\nexport class EvolutionPipeline {\n private readonly detector: IntentDetector;\n private readonly aggregator: IntentAggregator;\n private readonly emitter: EventEmitter;\n private readonly onIntent?: (intent: IntentSignal) => Promise<void> | void;\n private readonly onSnapshot?: (\n snapshot: IntentAggregatorSnapshot\n ) => Promise<void> | void;\n private timer?: NodeJS.Timeout;\n private previousMetrics?: ReturnType<IntentAggregator['flush']>['metrics'];\n\n constructor(options: EvolutionPipelineOptions = {}) {\n this.detector = options.detector ?? new IntentDetector();\n this.aggregator = options.aggregator ?? new IntentAggregator();\n this.emitter = options.emitter ?? new EventEmitter();\n this.onIntent = options.onIntent;\n this.onSnapshot = options.onSnapshot;\n }\n\n ingest(sample: TelemetrySample) {\n this.aggregator.add(sample);\n }\n\n on(listener: (event: EvolutionPipelineEvent) => void) {\n this.emitter.on('event', listener);\n }\n\n start(intervalMs = 5 * 60 * 1000) {\n this.stop();\n this.timer = setInterval(() => {\n void this.run();\n }, intervalMs);\n }\n\n stop() {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n }\n\n async run() {\n const snapshot = this.aggregator.flush();\n this.emit({\n type: 'telemetry.window',\n payload: { sampleCount: snapshot.sampleCount },\n });\n if (this.onSnapshot) await this.onSnapshot(snapshot);\n if (!snapshot.sampleCount) return;\n\n const metricSignals = this.detector.detectFromMetrics(\n snapshot.metrics,\n this.previousMetrics\n );\n const sequenceSignals = this.detector.detectSequentialIntents(\n snapshot.sequences\n );\n this.previousMetrics = snapshot.metrics;\n\n const signals = [...metricSignals, ...sequenceSignals];\n for (const signal of signals) {\n if (this.onIntent) await this.onIntent(signal);\n this.emit({ type: 'intent.detected', payload: signal });\n }\n }\n\n private emit(event: EvolutionPipelineEvent) {\n this.emitter.emit('event', event);\n }\n}\n"],"mappings":"+JAoBA,IAAa,EAAb,KAA+B,CAC7B,SACA,WACA,QACA,SACA,WAGA,MACA,gBAEA,YAAY,EAAoC,EAAE,CAAE,CAClD,KAAK,SAAW,EAAQ,UAAY,IAAI,EACxC,KAAK,WAAa,EAAQ,YAAc,IAAI,EAC5C,KAAK,QAAU,EAAQ,SAAW,IAAI,EACtC,KAAK,SAAW,EAAQ,SACxB,KAAK,WAAa,EAAQ,WAG5B,OAAO,EAAyB,CAC9B,KAAK,WAAW,IAAI,EAAO,CAG7B,GAAG,EAAmD,CACpD,KAAK,QAAQ,GAAG,QAAS,EAAS,CAGpC,MAAM,EAAa,IAAS,IAAM,CAChC,KAAK,MAAM,CACX,KAAK,MAAQ,gBAAkB,CACxB,KAAK,KAAK,EACd,EAAW,CAGhB,MAAO,CACL,AAEE,KAAK,SADL,cAAc,KAAK,MAAM,CACZ,IAAA,IAIjB,MAAM,KAAM,CACV,IAAM,EAAW,KAAK,WAAW,OAAO,CAMxC,GALA,KAAK,KAAK,CACR,KAAM,mBACN,QAAS,CAAE,YAAa,EAAS,YAAa,CAC/C,CAAC,CACE,KAAK,YAAY,MAAM,KAAK,WAAW,EAAS,CAChD,CAAC,EAAS,YAAa,OAE3B,IAAM,EAAgB,KAAK,SAAS,kBAClC,EAAS,QACT,KAAK,gBACN,CACK,EAAkB,KAAK,SAAS,wBACpC,EAAS,UACV,CACD,KAAK,gBAAkB,EAAS,QAEhC,IAAM,EAAU,CAAC,GAAG,EAAe,GAAG,EAAgB,CACtD,IAAK,IAAM,KAAU,EACf,KAAK,UAAU,MAAM,KAAK,SAAS,EAAO,CAC9C,KAAK,KAAK,CAAE,KAAM,kBAAmB,QAAS,EAAQ,CAAC,CAI3D,KAAa,EAA+B,CAC1C,KAAK,QAAQ,KAAK,QAAS,EAAM"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { LifecycleAssessment, LifecycleStage } from "@lssm/lib.lifecycle";
|
|
3
|
+
|
|
4
|
+
//#region src/pipeline/lifecycle-pipeline.d.ts
|
|
5
|
+
type LifecyclePipelineEvent = {
|
|
6
|
+
type: 'assessment.recorded';
|
|
7
|
+
payload: {
|
|
8
|
+
tenantId?: string;
|
|
9
|
+
stage: LifecycleStage;
|
|
10
|
+
};
|
|
11
|
+
} | {
|
|
12
|
+
type: 'stage.changed';
|
|
13
|
+
payload: {
|
|
14
|
+
tenantId?: string;
|
|
15
|
+
previousStage?: LifecycleStage;
|
|
16
|
+
nextStage: LifecycleStage;
|
|
17
|
+
};
|
|
18
|
+
} | {
|
|
19
|
+
type: 'confidence.low';
|
|
20
|
+
payload: {
|
|
21
|
+
tenantId?: string;
|
|
22
|
+
confidence: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
interface LifecycleKpiPipelineOptions {
|
|
26
|
+
meterName?: string;
|
|
27
|
+
emitter?: EventEmitter;
|
|
28
|
+
lowConfidenceThreshold?: number;
|
|
29
|
+
}
|
|
30
|
+
declare class LifecycleKpiPipeline {
|
|
31
|
+
private readonly assessmentCounter;
|
|
32
|
+
private readonly confidenceHistogram;
|
|
33
|
+
private readonly stageUpDownCounter;
|
|
34
|
+
private readonly emitter;
|
|
35
|
+
private readonly lowConfidenceThreshold;
|
|
36
|
+
private readonly currentStageByTenant;
|
|
37
|
+
constructor(options?: LifecycleKpiPipelineOptions);
|
|
38
|
+
recordAssessment(assessment: LifecycleAssessment, tenantId?: string): void;
|
|
39
|
+
on(listener: (event: LifecyclePipelineEvent) => void): void;
|
|
40
|
+
private ensureStageCounters;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { LifecycleKpiPipeline, LifecycleKpiPipelineOptions, LifecyclePipelineEvent };
|
|
44
|
+
//# sourceMappingURL=lifecycle-pipeline.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-pipeline.d.mts","names":[],"sources":["../../src/pipeline/lifecycle-pipeline.ts"],"sourcesContent":[],"mappings":";;;;KASY,sBAAA;;EAAA,OAAA,EAAA;IAG+B,QAAA,CAAA,EAAA,MAAA;IAMnB,KAAA,EANmB,cAMnB;EACL,CAAA;CAAc,GAAA;EAQhB,IAAA,EAAA,eAAA;EAMJ,OAAA,EAAA;IAQU,QAAA,CAAA,EAAA,MAAA;IAqBQ,aAAA,CAAA,EA5CP,cA4CO;IAoBR,SAAA,EA/DJ,cA+DI;EAAsB,CAAA;;;;;;;;UAvD5B,2BAAA;;YAEL;;;cAIC,oBAAA;;;;;;;wBAQU;+BAqBQ;uBAoBR"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createCounter as e,createHistogram as t,createUpDownCounter as n}from"../metrics/index.mjs";import{o as r}from"../lifecycle/dist/utils/formatters.mjs";import"../lifecycle/dist/index.mjs";import{EventEmitter as i}from"node:events";var a=class{assessmentCounter;confidenceHistogram;stageUpDownCounter;emitter;lowConfidenceThreshold;currentStageByTenant=new Map;constructor(r={}){let a=r.meterName??`@lssm/lib.lifecycle-kpi`;this.assessmentCounter=e(`lifecycle_assessments_total`,`Total lifecycle assessments`,a),this.confidenceHistogram=t(`lifecycle_assessment_confidence`,`Lifecycle assessment confidence distribution`,a),this.stageUpDownCounter=n(`lifecycle_stage_tenants`,`Current tenants per lifecycle stage`,a),this.emitter=r.emitter??new i,this.lowConfidenceThreshold=r.lowConfidenceThreshold??.4}recordAssessment(e,t){let n={stage:r(e.stage),tenantId:t};this.assessmentCounter.add(1,n),this.confidenceHistogram.record(e.confidence,n),this.ensureStageCounters(e.stage,t),this.emitter.emit(`event`,{type:`assessment.recorded`,payload:{tenantId:t,stage:e.stage}}),e.confidence<this.lowConfidenceThreshold&&this.emitter.emit(`event`,{type:`confidence.low`,payload:{tenantId:t,confidence:e.confidence}})}on(e){this.emitter.on(`event`,e)}ensureStageCounters(e,t){if(!t)return;let n=this.currentStageByTenant.get(t);n!==e&&(n!==void 0&&this.stageUpDownCounter.add(-1,{stage:r(n),tenantId:t}),this.stageUpDownCounter.add(1,{stage:r(e),tenantId:t}),this.currentStageByTenant.set(t,e),this.emitter.emit(`event`,{type:`stage.changed`,payload:{tenantId:t,previousStage:n,nextStage:e}}))}};export{a as LifecycleKpiPipeline};
|
|
2
|
+
//# sourceMappingURL=lifecycle-pipeline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle-pipeline.mjs","names":["getStageLabel"],"sources":["../../src/pipeline/lifecycle-pipeline.ts"],"sourcesContent":["import { EventEmitter } from 'node:events';\nimport type { LifecycleAssessment, LifecycleStage } from '@lssm/lib.lifecycle';\nimport { getStageLabel } from '@lssm/lib.lifecycle';\nimport {\n createCounter,\n createHistogram,\n createUpDownCounter,\n} from '../metrics';\n\nexport type LifecyclePipelineEvent =\n | {\n type: 'assessment.recorded';\n payload: { tenantId?: string; stage: LifecycleStage };\n }\n | {\n type: 'stage.changed';\n payload: {\n tenantId?: string;\n previousStage?: LifecycleStage;\n nextStage: LifecycleStage;\n };\n }\n | {\n type: 'confidence.low';\n payload: { tenantId?: string; confidence: number };\n };\n\nexport interface LifecycleKpiPipelineOptions {\n meterName?: string;\n emitter?: EventEmitter;\n lowConfidenceThreshold?: number;\n}\n\nexport class LifecycleKpiPipeline {\n private readonly assessmentCounter;\n private readonly confidenceHistogram;\n private readonly stageUpDownCounter;\n private readonly emitter: EventEmitter;\n private readonly lowConfidenceThreshold: number;\n private readonly currentStageByTenant = new Map<string, LifecycleStage>();\n\n constructor(options: LifecycleKpiPipelineOptions = {}) {\n const meterName = options.meterName ?? '@lssm/lib.lifecycle-kpi';\n this.assessmentCounter = createCounter(\n 'lifecycle_assessments_total',\n 'Total lifecycle assessments',\n meterName\n );\n this.confidenceHistogram = createHistogram(\n 'lifecycle_assessment_confidence',\n 'Lifecycle assessment confidence distribution',\n meterName\n );\n this.stageUpDownCounter = createUpDownCounter(\n 'lifecycle_stage_tenants',\n 'Current tenants per lifecycle stage',\n meterName\n );\n this.emitter = options.emitter ?? new EventEmitter();\n this.lowConfidenceThreshold = options.lowConfidenceThreshold ?? 0.4;\n }\n\n recordAssessment(assessment: LifecycleAssessment, tenantId?: string) {\n const stageLabel = getStageLabel(assessment.stage);\n const attributes = { stage: stageLabel, tenantId };\n this.assessmentCounter.add(1, attributes);\n this.confidenceHistogram.record(assessment.confidence, attributes);\n\n this.ensureStageCounters(assessment.stage, tenantId);\n this.emitter.emit('event', {\n type: 'assessment.recorded',\n payload: { tenantId, stage: assessment.stage },\n } satisfies LifecyclePipelineEvent);\n\n if (assessment.confidence < this.lowConfidenceThreshold) {\n this.emitter.emit('event', {\n type: 'confidence.low',\n payload: { tenantId, confidence: assessment.confidence },\n } satisfies LifecyclePipelineEvent);\n }\n }\n\n on(listener: (event: LifecyclePipelineEvent) => void) {\n this.emitter.on('event', listener);\n }\n\n private ensureStageCounters(stage: LifecycleStage, tenantId?: string) {\n if (!tenantId) return;\n const previous = this.currentStageByTenant.get(tenantId);\n if (previous === stage) return;\n\n if (previous !== undefined) {\n this.stageUpDownCounter.add(-1, {\n stage: getStageLabel(previous),\n tenantId,\n });\n }\n this.stageUpDownCounter.add(1, { stage: getStageLabel(stage), tenantId });\n this.currentStageByTenant.set(tenantId, stage);\n this.emitter.emit('event', {\n type: 'stage.changed',\n payload: { tenantId, previousStage: previous, nextStage: stage },\n } satisfies LifecyclePipelineEvent);\n }\n}\n"],"mappings":"6OAiCA,IAAa,EAAb,KAAkC,CAChC,kBACA,oBACA,mBACA,QACA,uBACA,qBAAwC,IAAI,IAE5C,YAAY,EAAuC,EAAE,CAAE,CACrD,IAAM,EAAY,EAAQ,WAAa,0BACvC,KAAK,kBAAoB,EACvB,8BACA,8BACA,EACD,CACD,KAAK,oBAAsB,EACzB,kCACA,+CACA,EACD,CACD,KAAK,mBAAqB,EACxB,0BACA,sCACA,EACD,CACD,KAAK,QAAU,EAAQ,SAAW,IAAI,EACtC,KAAK,uBAAyB,EAAQ,wBAA0B,GAGlE,iBAAiB,EAAiC,EAAmB,CAEnE,IAAM,EAAa,CAAE,MADFA,EAAc,EAAW,MAAM,CACV,WAAU,CAClD,KAAK,kBAAkB,IAAI,EAAG,EAAW,CACzC,KAAK,oBAAoB,OAAO,EAAW,WAAY,EAAW,CAElE,KAAK,oBAAoB,EAAW,MAAO,EAAS,CACpD,KAAK,QAAQ,KAAK,QAAS,CACzB,KAAM,sBACN,QAAS,CAAE,WAAU,MAAO,EAAW,MAAO,CAC/C,CAAkC,CAE/B,EAAW,WAAa,KAAK,wBAC/B,KAAK,QAAQ,KAAK,QAAS,CACzB,KAAM,iBACN,QAAS,CAAE,WAAU,WAAY,EAAW,WAAY,CACzD,CAAkC,CAIvC,GAAG,EAAmD,CACpD,KAAK,QAAQ,GAAG,QAAS,EAAS,CAGpC,oBAA4B,EAAuB,EAAmB,CACpE,GAAI,CAAC,EAAU,OACf,IAAM,EAAW,KAAK,qBAAqB,IAAI,EAAS,CACpD,IAAa,IAEb,IAAa,IAAA,IACf,KAAK,mBAAmB,IAAI,GAAI,CAC9B,MAAOA,EAAc,EAAS,CAC9B,WACD,CAAC,CAEJ,KAAK,mBAAmB,IAAI,EAAG,CAAE,MAAOA,EAAc,EAAM,CAAE,WAAU,CAAC,CACzE,KAAK,qBAAqB,IAAI,EAAU,EAAM,CAC9C,KAAK,QAAQ,KAAK,QAAS,CACzB,KAAM,gBACN,QAAS,CAAE,WAAU,cAAe,EAAU,UAAW,EAAO,CACjE,CAAkC"}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import { TelemetrySample } from "../intent/aggregator.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/tracing/middleware.d.ts
|
|
2
|
-
|
|
4
|
+
interface TracingMiddlewareOptions {
|
|
5
|
+
resolveOperation?: (input: {
|
|
6
|
+
req: Request;
|
|
7
|
+
res?: Response;
|
|
8
|
+
}) => {
|
|
9
|
+
name: string;
|
|
10
|
+
version: number;
|
|
11
|
+
} | undefined;
|
|
12
|
+
onSample?: (sample: TelemetrySample) => void;
|
|
13
|
+
tenantResolver?: (req: Request) => string | undefined;
|
|
14
|
+
actorResolver?: (req: Request) => string | undefined;
|
|
15
|
+
}
|
|
16
|
+
declare function createTracingMiddleware(options?: TracingMiddlewareOptions): (req: Request, next: () => Promise<Response>) => Promise<Response>;
|
|
3
17
|
//#endregion
|
|
4
|
-
export { createTracingMiddleware };
|
|
18
|
+
export { TracingMiddlewareOptions, createTracingMiddleware };
|
|
5
19
|
//# sourceMappingURL=middleware.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/tracing/middleware.ts"],"sourcesContent":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../../src/tracing/middleware.ts"],"sourcesContent":[],"mappings":";;;UAKiB,wBAAA;;IAAA,GAAA,EAER,OAFQ;IAER,GAAA,CAAA,EACC,QADD;EACC,CAAA,EAAA,GAAA;IAEY,IAAA,EAAA,MAAA;IACG,OAAA,EAAA,MAAA;EACD,CAAA,GAAA,SAAA;EAAO,QAAA,CAAA,EAAA,CAAA,MAAA,EAFT,eAES,EAAA,GAAA,IAAA;EAGf,cAAA,CAAA,EAAA,CAAA,GAAA,EAJS,OAIc,EAAA,GAAA,MAAA,GAAA,SAAA;EAC5B,aAAA,CAAA,EAAA,CAAA,GAAA,EAJa,OAIb,EAAA,GAAA,MAAA,GAAA,SAAA;;AAEuC,iBAHlC,uBAAA,CAGkC,OAAA,CAAA,EAFvC,wBAEuC,CAAA,EAAA,CAAA,GAAA,EAA7B,OAA6B,EAAA,IAAA,EAAA,GAAA,GAAR,OAAQ,CAAA,QAAA,CAAA,EAAA,GAAS,OAAT,CAAS,QAAT,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{traceAsync as e}from"./index.mjs";import{standardMetrics as t}from"../metrics/index.mjs";function n(){return async(
|
|
1
|
+
import{traceAsync as e}from"./index.mjs";import{standardMetrics as t}from"../metrics/index.mjs";function n(n={}){return async(i,a)=>{let o=i.method,s=new URL(i.url).pathname;t.httpRequests.add(1,{method:o,path:s});let c=performance.now();return e(`HTTP ${o} ${s}`,async e=>{e.setAttribute(`http.method`,o),e.setAttribute(`http.url`,i.url);try{let l=await a();e.setAttribute(`http.status_code`,l.status);let u=(performance.now()-c)/1e3;return t.httpDuration.record(u,{method:o,path:s,status:l.status.toString()}),r({req:i,res:l,span:e,success:!0,durationMs:u*1e3,options:n}),l}catch(a){throw t.operationErrors.add(1,{method:o,path:s}),r({req:i,span:e,success:!1,durationMs:performance.now()-c,error:a,options:n}),a}})}}function r({req:e,res:t,span:n,success:r,durationMs:i,error:a,options:o}){if(!o.onSample||!o.resolveOperation)return;let s=o.resolveOperation({req:e,res:t});if(!s)return;let c={operation:s,durationMs:i,success:r,timestamp:new Date,errorCode:!r&&a instanceof Error?a.name:r?void 0:`unknown`,tenantId:o.tenantResolver?.(e),actorId:o.actorResolver?.(e),traceId:n.spanContext().traceId,metadata:{method:e.method,path:new URL(e.url).pathname,status:t?.status}};o.onSample(c)}export{n as createTracingMiddleware};
|
|
2
2
|
//# sourceMappingURL=middleware.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.mjs","names":[],"sources":["../../src/tracing/middleware.ts"],"sourcesContent":["import { traceAsync } from './index';\nimport { standardMetrics } from '../metrics';\n\nexport function createTracingMiddleware() {\n return async (req: Request, next: () => Promise<Response>) => {\n const method = req.method;\n const url = new URL(req.url);\n const path = url.pathname;\n\n standardMetrics.httpRequests.add(1, { method, path });\n const startTime = performance.now();\n\n return traceAsync(`HTTP ${method} ${path}`, async (span) => {\n span.setAttribute('http.method', method);\n span.setAttribute('http.url', req.url);\n\n try {\n const response = await next();\n span.setAttribute('http.status_code', response.status);\n\n const duration = (performance.now() - startTime) / 1000;\n standardMetrics.httpDuration.record(duration, {\n method,\n path,\n status: response.status.toString(),\n });\n\n return response;\n } catch (error) {\n standardMetrics.operationErrors.add(1, { method, path });\n throw error;\n }\n });\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"middleware.mjs","names":["sample: TelemetrySample"],"sources":["../../src/tracing/middleware.ts"],"sourcesContent":["import type { Span } from '@opentelemetry/api';\nimport { traceAsync } from './index';\nimport { standardMetrics } from '../metrics';\nimport type { TelemetrySample } from '../intent/aggregator';\n\nexport interface TracingMiddlewareOptions {\n resolveOperation?: (input: {\n req: Request;\n res?: Response;\n }) => { name: string; version: number } | undefined;\n onSample?: (sample: TelemetrySample) => void;\n tenantResolver?: (req: Request) => string | undefined;\n actorResolver?: (req: Request) => string | undefined;\n}\n\nexport function createTracingMiddleware(\n options: TracingMiddlewareOptions = {}\n) {\n return async (req: Request, next: () => Promise<Response>) => {\n const method = req.method;\n const url = new URL(req.url);\n const path = url.pathname;\n\n standardMetrics.httpRequests.add(1, { method, path });\n const startTime = performance.now();\n\n return traceAsync(`HTTP ${method} ${path}`, async (span) => {\n span.setAttribute('http.method', method);\n span.setAttribute('http.url', req.url);\n\n try {\n const response = await next();\n span.setAttribute('http.status_code', response.status);\n\n const duration = (performance.now() - startTime) / 1000;\n standardMetrics.httpDuration.record(duration, {\n method,\n path,\n status: response.status.toString(),\n });\n\n emitTelemetrySample({\n req,\n res: response,\n span,\n success: true,\n durationMs: duration * 1000,\n options,\n });\n\n return response;\n } catch (error) {\n standardMetrics.operationErrors.add(1, { method, path });\n emitTelemetrySample({\n req,\n span,\n success: false,\n durationMs: performance.now() - startTime,\n error,\n options,\n });\n throw error;\n }\n });\n };\n}\n\ninterface EmitTelemetryArgs {\n req: Request;\n res?: Response;\n span: Span;\n success: boolean;\n durationMs: number;\n error?: unknown;\n options: TracingMiddlewareOptions;\n}\n\nfunction emitTelemetrySample({\n req,\n res,\n span,\n success,\n durationMs,\n error,\n options,\n}: EmitTelemetryArgs) {\n if (!options.onSample || !options.resolveOperation) return;\n const operation = options.resolveOperation({ req, res });\n if (!operation) return;\n const sample: TelemetrySample = {\n operation,\n durationMs,\n success,\n timestamp: new Date(),\n errorCode:\n !success && error instanceof Error\n ? error.name\n : success\n ? undefined\n : 'unknown',\n tenantId: options.tenantResolver?.(req),\n actorId: options.actorResolver?.(req),\n traceId: span.spanContext().traceId,\n metadata: {\n method: req.method,\n path: new URL(req.url).pathname,\n status: res?.status,\n },\n };\n options.onSample(sample);\n}\n"],"mappings":"gGAeA,SAAgB,EACd,EAAoC,EAAE,CACtC,CACA,OAAO,MAAO,EAAc,IAAkC,CAC5D,IAAM,EAAS,EAAI,OAEb,EADM,IAAI,IAAI,EAAI,IAAI,CACX,SAEjB,EAAgB,aAAa,IAAI,EAAG,CAAE,SAAQ,OAAM,CAAC,CACrD,IAAM,EAAY,YAAY,KAAK,CAEnC,OAAO,EAAW,QAAQ,EAAO,GAAG,IAAQ,KAAO,IAAS,CAC1D,EAAK,aAAa,cAAe,EAAO,CACxC,EAAK,aAAa,WAAY,EAAI,IAAI,CAEtC,GAAI,CACF,IAAM,EAAW,MAAM,GAAM,CAC7B,EAAK,aAAa,mBAAoB,EAAS,OAAO,CAEtD,IAAM,GAAY,YAAY,KAAK,CAAG,GAAa,IAgBnD,OAfA,EAAgB,aAAa,OAAO,EAAU,CAC5C,SACA,OACA,OAAQ,EAAS,OAAO,UAAU,CACnC,CAAC,CAEF,EAAoB,CAClB,MACA,IAAK,EACL,OACA,QAAS,GACT,WAAY,EAAW,IACvB,UACD,CAAC,CAEK,QACA,EAAO,CAUd,MATA,EAAgB,gBAAgB,IAAI,EAAG,CAAE,SAAQ,OAAM,CAAC,CACxD,EAAoB,CAClB,MACA,OACA,QAAS,GACT,WAAY,YAAY,KAAK,CAAG,EAChC,QACA,UACD,CAAC,CACI,IAER,EAcN,SAAS,EAAoB,CAC3B,MACA,MACA,OACA,UACA,aACA,QACA,WACoB,CACpB,GAAI,CAAC,EAAQ,UAAY,CAAC,EAAQ,iBAAkB,OACpD,IAAM,EAAY,EAAQ,iBAAiB,CAAE,MAAK,MAAK,CAAC,CACxD,GAAI,CAAC,EAAW,OAChB,IAAMA,EAA0B,CAC9B,YACA,aACA,UACA,UAAW,IAAI,KACf,UACE,CAAC,GAAW,aAAiB,MACzB,EAAM,KACN,EACE,IAAA,GACA,UACR,SAAU,EAAQ,iBAAiB,EAAI,CACvC,QAAS,EAAQ,gBAAgB,EAAI,CACrC,QAAS,EAAK,aAAa,CAAC,QAC5B,SAAU,CACR,OAAQ,EAAI,OACZ,KAAM,IAAI,IAAI,EAAI,IAAI,CAAC,SACvB,OAAQ,GAAK,OACd,CACF,CACD,EAAQ,SAAS,EAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lssm/lib.observability",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"main": "./dist/index.mjs",
|
|
5
5
|
"types": "./dist/index.d.mts",
|
|
6
6
|
"scripts": {
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"lint:check": "eslint src",
|
|
15
15
|
"test": "vitest run"
|
|
16
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@lssm/lib.lifecycle": "workspace:*"
|
|
19
|
+
},
|
|
17
20
|
"peerDependencies": {
|
|
18
21
|
"@opentelemetry/api": "*"
|
|
19
22
|
},
|
|
@@ -23,8 +26,16 @@
|
|
|
23
26
|
},
|
|
24
27
|
"exports": {
|
|
25
28
|
".": "./dist/index.mjs",
|
|
29
|
+
"./anomaly/alert-manager": "./dist/anomaly/alert-manager.mjs",
|
|
30
|
+
"./anomaly/anomaly-detector": "./dist/anomaly/anomaly-detector.mjs",
|
|
31
|
+
"./anomaly/baseline-calculator": "./dist/anomaly/baseline-calculator.mjs",
|
|
32
|
+
"./anomaly/root-cause-analyzer": "./dist/anomaly/root-cause-analyzer.mjs",
|
|
33
|
+
"./intent/aggregator": "./dist/intent/aggregator.mjs",
|
|
34
|
+
"./intent/detector": "./dist/intent/detector.mjs",
|
|
26
35
|
"./logging": "./dist/logging/index.mjs",
|
|
27
36
|
"./metrics": "./dist/metrics/index.mjs",
|
|
37
|
+
"./pipeline/evolution-pipeline": "./dist/pipeline/evolution-pipeline.mjs",
|
|
38
|
+
"./pipeline/lifecycle-pipeline": "./dist/pipeline/lifecycle-pipeline.mjs",
|
|
28
39
|
"./tracing": "./dist/tracing/index.mjs",
|
|
29
40
|
"./tracing/middleware": "./dist/tracing/middleware.mjs",
|
|
30
41
|
"./*": "./*"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AnomalySignal } from './anomaly-detector';
|
|
2
|
+
import type { RootCauseAnalysis } from './root-cause-analyzer';
|
|
3
|
+
|
|
4
|
+
export interface AlertManagerOptions {
|
|
5
|
+
cooldownMs?: number;
|
|
6
|
+
transport: (payload: {
|
|
7
|
+
signal: AnomalySignal;
|
|
8
|
+
analysis: RootCauseAnalysis;
|
|
9
|
+
}) => Promise<void> | void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AlertManager {
|
|
13
|
+
private readonly cooldownMs: number;
|
|
14
|
+
private readonly lastAlert = new Map<string, number>();
|
|
15
|
+
|
|
16
|
+
constructor(private readonly options: AlertManagerOptions) {
|
|
17
|
+
this.cooldownMs = options.cooldownMs ?? 60_000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async notify(signal: AnomalySignal, analysis: RootCauseAnalysis) {
|
|
21
|
+
const key = `${signal.type}:${analysis.culprit?.id ?? 'none'}`;
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const last = this.lastAlert.get(key) ?? 0;
|
|
24
|
+
if (now - last < this.cooldownMs) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await this.options.transport({ signal, analysis });
|
|
29
|
+
this.lastAlert.set(key, now);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BaselineCalculator, type MetricPoint } from './baseline-calculator';
|
|
2
|
+
|
|
3
|
+
export interface AnomalyThresholds {
|
|
4
|
+
errorRateDelta?: number;
|
|
5
|
+
latencyDelta?: number;
|
|
6
|
+
throughputDrop?: number;
|
|
7
|
+
minSamples?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AnomalySignal {
|
|
11
|
+
type: 'error_rate_spike' | 'latency_regression' | 'throughput_drop';
|
|
12
|
+
delta: number;
|
|
13
|
+
point: MetricPoint;
|
|
14
|
+
baseline: ReturnType<BaselineCalculator['getSnapshot']>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class AnomalyDetector {
|
|
18
|
+
private readonly baseline: BaselineCalculator;
|
|
19
|
+
private readonly thresholds: Required<AnomalyThresholds> = {
|
|
20
|
+
errorRateDelta: 0.5,
|
|
21
|
+
latencyDelta: 0.35,
|
|
22
|
+
throughputDrop: 0.4,
|
|
23
|
+
minSamples: 10,
|
|
24
|
+
} as Required<AnomalyThresholds>;
|
|
25
|
+
|
|
26
|
+
constructor(options: AnomalyThresholds = {}) {
|
|
27
|
+
this.baseline = new BaselineCalculator();
|
|
28
|
+
this.thresholds = { ...this.thresholds, ...options };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
evaluate(point: MetricPoint): AnomalySignal[] {
|
|
32
|
+
const baselineSnapshot = this.baseline.update(point);
|
|
33
|
+
if (baselineSnapshot.sampleCount < this.thresholds.minSamples) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const signals: AnomalySignal[] = [];
|
|
38
|
+
|
|
39
|
+
const errorDelta = this.relativeDelta(
|
|
40
|
+
point.errorRate,
|
|
41
|
+
baselineSnapshot.errorRate
|
|
42
|
+
);
|
|
43
|
+
if (errorDelta > this.thresholds.errorRateDelta) {
|
|
44
|
+
signals.push({
|
|
45
|
+
type: 'error_rate_spike',
|
|
46
|
+
delta: errorDelta,
|
|
47
|
+
point,
|
|
48
|
+
baseline: baselineSnapshot,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const latencyDelta = this.relativeDelta(
|
|
53
|
+
point.latencyP99,
|
|
54
|
+
baselineSnapshot.latencyP99
|
|
55
|
+
);
|
|
56
|
+
if (latencyDelta > this.thresholds.latencyDelta) {
|
|
57
|
+
signals.push({
|
|
58
|
+
type: 'latency_regression',
|
|
59
|
+
delta: latencyDelta,
|
|
60
|
+
point,
|
|
61
|
+
baseline: baselineSnapshot,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const throughputDelta = this.relativeDrop(
|
|
66
|
+
point.throughput,
|
|
67
|
+
baselineSnapshot.throughput
|
|
68
|
+
);
|
|
69
|
+
if (throughputDelta > this.thresholds.throughputDrop) {
|
|
70
|
+
signals.push({
|
|
71
|
+
type: 'throughput_drop',
|
|
72
|
+
delta: throughputDelta,
|
|
73
|
+
point,
|
|
74
|
+
baseline: baselineSnapshot,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return signals;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private relativeDelta(value: number, baseline: number) {
|
|
82
|
+
if (baseline === 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
return (value - baseline) / baseline;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private relativeDrop(value: number, baseline: number) {
|
|
89
|
+
if (baseline === 0) {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
return (baseline - value) / baseline;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface MetricPoint {
|
|
2
|
+
latencyP99: number;
|
|
3
|
+
latencyP95: number;
|
|
4
|
+
errorRate: number;
|
|
5
|
+
throughput: number;
|
|
6
|
+
timestamp: Date;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BaselineSnapshot {
|
|
10
|
+
latencyP99: number;
|
|
11
|
+
latencyP95: number;
|
|
12
|
+
errorRate: number;
|
|
13
|
+
throughput: number;
|
|
14
|
+
sampleCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class BaselineCalculator {
|
|
18
|
+
private snapshot: BaselineSnapshot = {
|
|
19
|
+
latencyP99: 0,
|
|
20
|
+
latencyP95: 0,
|
|
21
|
+
errorRate: 0,
|
|
22
|
+
throughput: 0,
|
|
23
|
+
sampleCount: 0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
constructor(private readonly alpha = 0.2) {}
|
|
27
|
+
|
|
28
|
+
update(point: MetricPoint): BaselineSnapshot {
|
|
29
|
+
const { sampleCount } = this.snapshot;
|
|
30
|
+
const nextCount = sampleCount + 1;
|
|
31
|
+
const weight = sampleCount === 0 ? 1 : this.alpha;
|
|
32
|
+
|
|
33
|
+
this.snapshot = {
|
|
34
|
+
latencyP99: this.mix(this.snapshot.latencyP99, point.latencyP99, weight),
|
|
35
|
+
latencyP95: this.mix(this.snapshot.latencyP95, point.latencyP95, weight),
|
|
36
|
+
errorRate: this.mix(this.snapshot.errorRate, point.errorRate, weight),
|
|
37
|
+
throughput: this.mix(this.snapshot.throughput, point.throughput, weight),
|
|
38
|
+
sampleCount: nextCount,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return this.snapshot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getSnapshot() {
|
|
45
|
+
return this.snapshot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private mix(current: number, next: number, weight: number) {
|
|
49
|
+
if (this.snapshot.sampleCount === 0) {
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
return current * (1 - weight) + next * weight;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AnomalySignal } from './anomaly-detector';
|
|
2
|
+
|
|
3
|
+
export interface DeploymentEvent {
|
|
4
|
+
id: string;
|
|
5
|
+
operation: string;
|
|
6
|
+
deployedAt: Date;
|
|
7
|
+
stage?: string;
|
|
8
|
+
status: 'in_progress' | 'completed' | 'rolled_back';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RootCauseAnalysis {
|
|
12
|
+
signal: AnomalySignal;
|
|
13
|
+
culprit?: DeploymentEvent;
|
|
14
|
+
notes: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class RootCauseAnalyzer {
|
|
18
|
+
constructor(private readonly lookbackMs: number = 15 * 60 * 1000) {}
|
|
19
|
+
|
|
20
|
+
analyze(
|
|
21
|
+
signal: AnomalySignal,
|
|
22
|
+
deployments: DeploymentEvent[]
|
|
23
|
+
): RootCauseAnalysis {
|
|
24
|
+
const windowStart = new Date(
|
|
25
|
+
signal.point.timestamp.getTime() - this.lookbackMs
|
|
26
|
+
);
|
|
27
|
+
const candidates = deployments
|
|
28
|
+
.filter((deployment) => deployment.deployedAt >= windowStart)
|
|
29
|
+
.sort((a, b) => b.deployedAt.getTime() - a.deployedAt.getTime());
|
|
30
|
+
|
|
31
|
+
const notes: string[] = [];
|
|
32
|
+
let culprit: DeploymentEvent | undefined;
|
|
33
|
+
|
|
34
|
+
if (candidates.length > 0) {
|
|
35
|
+
culprit = candidates[0]!;
|
|
36
|
+
notes.push(
|
|
37
|
+
`Closest deployment ${culprit.id} (${culprit.operation}) at ${culprit.deployedAt.toISOString()}`
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
notes.push('No deployments found within lookback window.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (signal.type === 'latency_regression') {
|
|
44
|
+
notes.push(
|
|
45
|
+
'Verify recent schema changes and external dependency latency.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (signal.type === 'error_rate_spike') {
|
|
50
|
+
notes.push('Check SLO monitor for correlated incidents.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { signal, culprit, notes };
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,41 @@ export {
|
|
|
7
7
|
standardMetrics,
|
|
8
8
|
} from './metrics';
|
|
9
9
|
export { Logger, logger } from './logging';
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createTracingMiddleware,
|
|
12
|
+
type TracingMiddlewareOptions,
|
|
13
|
+
} from './tracing/middleware';
|
|
14
|
+
export {
|
|
15
|
+
IntentAggregator,
|
|
16
|
+
type IntentAggregatorSnapshot,
|
|
17
|
+
type TelemetrySample,
|
|
18
|
+
} from './intent/aggregator';
|
|
19
|
+
export {
|
|
20
|
+
IntentDetector,
|
|
21
|
+
type IntentSignal,
|
|
22
|
+
type IntentSignalType,
|
|
23
|
+
} from './intent/detector';
|
|
24
|
+
export {
|
|
25
|
+
EvolutionPipeline,
|
|
26
|
+
type EvolutionPipelineEvent,
|
|
27
|
+
type EvolutionPipelineOptions,
|
|
28
|
+
} from './pipeline/evolution-pipeline';
|
|
29
|
+
export {
|
|
30
|
+
LifecycleKpiPipeline,
|
|
31
|
+
type LifecycleKpiPipelineOptions,
|
|
32
|
+
type LifecyclePipelineEvent,
|
|
33
|
+
} from './pipeline/lifecycle-pipeline';
|
|
11
34
|
|
|
12
35
|
export type { LogLevel, LogEntry } from './logging';
|
|
36
|
+
|
|
37
|
+
export { BaselineCalculator } from './anomaly/baseline-calculator';
|
|
38
|
+
export {
|
|
39
|
+
AnomalyDetector,
|
|
40
|
+
type AnomalySignal,
|
|
41
|
+
type AnomalyThresholds,
|
|
42
|
+
} from './anomaly/anomaly-detector';
|
|
43
|
+
export {
|
|
44
|
+
RootCauseAnalyzer,
|
|
45
|
+
type RootCauseAnalysis,
|
|
46
|
+
} from './anomaly/root-cause-analyzer';
|
|
47
|
+
export { AlertManager } from './anomaly/alert-manager';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export interface TelemetrySample {
|
|
2
|
+
operation: { name: string; version: number };
|
|
3
|
+
durationMs: number;
|
|
4
|
+
success: boolean;
|
|
5
|
+
timestamp: Date;
|
|
6
|
+
errorCode?: string;
|
|
7
|
+
tenantId?: string;
|
|
8
|
+
traceId?: string;
|
|
9
|
+
actorId?: string;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AggregatedOperationMetrics {
|
|
14
|
+
operation: { name: string; version: number };
|
|
15
|
+
totalCalls: number;
|
|
16
|
+
successRate: number;
|
|
17
|
+
errorRate: number;
|
|
18
|
+
averageLatencyMs: number;
|
|
19
|
+
p95LatencyMs: number;
|
|
20
|
+
p99LatencyMs: number;
|
|
21
|
+
maxLatencyMs: number;
|
|
22
|
+
windowStart: Date;
|
|
23
|
+
windowEnd: Date;
|
|
24
|
+
topErrors: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OperationSequence {
|
|
28
|
+
steps: string[];
|
|
29
|
+
tenantId?: string;
|
|
30
|
+
count: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IntentAggregatorSnapshot {
|
|
34
|
+
metrics: AggregatedOperationMetrics[];
|
|
35
|
+
sequences: OperationSequence[];
|
|
36
|
+
sampleCount: number;
|
|
37
|
+
windowStart?: Date;
|
|
38
|
+
windowEnd?: Date;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface IntentAggregatorOptions {
|
|
42
|
+
windowMs?: number;
|
|
43
|
+
sequenceSampleSize?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
47
|
+
|
|
48
|
+
export class IntentAggregator {
|
|
49
|
+
private readonly windowMs: number;
|
|
50
|
+
private readonly sequenceSampleSize: number;
|
|
51
|
+
private readonly samples: TelemetrySample[] = [];
|
|
52
|
+
|
|
53
|
+
constructor(options: IntentAggregatorOptions = {}) {
|
|
54
|
+
this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
55
|
+
this.sequenceSampleSize = options.sequenceSampleSize ?? 1000;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
add(sample: TelemetrySample) {
|
|
59
|
+
this.samples.push(sample);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
flush(now = new Date()): IntentAggregatorSnapshot {
|
|
63
|
+
const minTimestamp = now.getTime() - this.windowMs;
|
|
64
|
+
const windowSamples = this.samples.filter(
|
|
65
|
+
(sample) => sample.timestamp.getTime() >= minTimestamp
|
|
66
|
+
);
|
|
67
|
+
this.samples.length = 0;
|
|
68
|
+
const metrics = this.aggregateMetrics(windowSamples);
|
|
69
|
+
const sequences = this.buildSequences(windowSamples);
|
|
70
|
+
const timestamps = windowSamples.map((sample) =>
|
|
71
|
+
sample.timestamp.getTime()
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
metrics,
|
|
75
|
+
sequences,
|
|
76
|
+
sampleCount: windowSamples.length,
|
|
77
|
+
windowStart: timestamps.length
|
|
78
|
+
? new Date(Math.min(...timestamps))
|
|
79
|
+
: undefined,
|
|
80
|
+
windowEnd: timestamps.length
|
|
81
|
+
? new Date(Math.max(...timestamps))
|
|
82
|
+
: undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private aggregateMetrics(samples: TelemetrySample[]) {
|
|
87
|
+
if (!samples.length) return [] as AggregatedOperationMetrics[];
|
|
88
|
+
const groups = new Map<string, TelemetrySample[]>();
|
|
89
|
+
for (const sample of samples) {
|
|
90
|
+
const key = `${sample.operation.name}.v${sample.operation.version}`;
|
|
91
|
+
const arr = groups.get(key) ?? [];
|
|
92
|
+
arr.push(sample);
|
|
93
|
+
groups.set(key, arr);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return [...groups.values()].map((group) => {
|
|
97
|
+
const durations = group.map((s) => s.durationMs).sort((a, b) => a - b);
|
|
98
|
+
const errors = group.filter((s) => !s.success);
|
|
99
|
+
const totalCalls = group.length;
|
|
100
|
+
const topErrors = errors.reduce<Record<string, number>>((acc, sample) => {
|
|
101
|
+
if (!sample.errorCode) return acc;
|
|
102
|
+
acc[sample.errorCode] = (acc[sample.errorCode] ?? 0) + 1;
|
|
103
|
+
return acc;
|
|
104
|
+
}, {});
|
|
105
|
+
const timestamps = group.map((s) => s.timestamp.getTime());
|
|
106
|
+
return {
|
|
107
|
+
operation: group[0]!.operation,
|
|
108
|
+
totalCalls,
|
|
109
|
+
successRate: (totalCalls - errors.length) / totalCalls,
|
|
110
|
+
errorRate: errors.length / totalCalls,
|
|
111
|
+
averageLatencyMs:
|
|
112
|
+
durations.reduce((sum, value) => sum + value, 0) / totalCalls,
|
|
113
|
+
p95LatencyMs: percentile(durations, 0.95),
|
|
114
|
+
p99LatencyMs: percentile(durations, 0.99),
|
|
115
|
+
maxLatencyMs: Math.max(...durations),
|
|
116
|
+
windowStart: new Date(Math.min(...timestamps)),
|
|
117
|
+
windowEnd: new Date(Math.max(...timestamps)),
|
|
118
|
+
topErrors,
|
|
119
|
+
} satisfies AggregatedOperationMetrics;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private buildSequences(samples: TelemetrySample[]): OperationSequence[] {
|
|
124
|
+
const byTrace = new Map<string, TelemetrySample[]>();
|
|
125
|
+
for (const sample of samples.slice(-this.sequenceSampleSize)) {
|
|
126
|
+
if (!sample.traceId) continue;
|
|
127
|
+
const arr = byTrace.get(sample.traceId) ?? [];
|
|
128
|
+
arr.push(sample);
|
|
129
|
+
byTrace.set(sample.traceId, arr);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sequences: Record<string, OperationSequence> = {};
|
|
133
|
+
for (const [traceId, events] of byTrace.entries()) {
|
|
134
|
+
const ordered = events.sort(
|
|
135
|
+
(a, b) => a.timestamp.getTime() - b.timestamp.getTime()
|
|
136
|
+
);
|
|
137
|
+
const steps = ordered.map((event) => event.operation.name);
|
|
138
|
+
if (steps.length < 2) continue;
|
|
139
|
+
const key = `${steps.join('>')}@${ordered[0]?.tenantId ?? 'global'}`;
|
|
140
|
+
const existing = sequences[key];
|
|
141
|
+
if (existing) {
|
|
142
|
+
existing.count += 1;
|
|
143
|
+
} else {
|
|
144
|
+
sequences[key] = {
|
|
145
|
+
steps,
|
|
146
|
+
tenantId: ordered[0]?.tenantId,
|
|
147
|
+
count: 1,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Object.values(sequences).sort((a, b) => b.count - a.count);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function percentile(values: number[], ratio: number) {
|
|
157
|
+
if (!values.length) return 0;
|
|
158
|
+
if (values.length === 1) return values[0]!;
|
|
159
|
+
const index = Math.min(values.length - 1, Math.floor(ratio * values.length));
|
|
160
|
+
return values[index]!;
|
|
161
|
+
}
|