@contractspec/lib.observability 3.7.16 → 3.7.18

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/anomaly/alert-manager.js +1 -23
  3. package/dist/anomaly/anomaly-detector.js +1 -101
  4. package/dist/anomaly/baseline-calculator.js +1 -39
  5. package/dist/anomaly/root-cause-analyzer.js +1 -31
  6. package/dist/index.js +5 -1128
  7. package/dist/intent/aggregator.js +1 -109
  8. package/dist/intent/detector.js +1 -132
  9. package/dist/logging/index.js +1 -41
  10. package/dist/metrics/index.js +1 -30
  11. package/dist/node/anomaly/alert-manager.js +1 -23
  12. package/dist/node/anomaly/anomaly-detector.js +1 -101
  13. package/dist/node/anomaly/baseline-calculator.js +1 -39
  14. package/dist/node/anomaly/root-cause-analyzer.js +1 -31
  15. package/dist/node/index.js +5 -1128
  16. package/dist/node/intent/aggregator.js +1 -109
  17. package/dist/node/intent/detector.js +1 -132
  18. package/dist/node/logging/index.js +1 -41
  19. package/dist/node/metrics/index.js +1 -30
  20. package/dist/node/pipeline/evolution-pipeline.js +1 -299
  21. package/dist/node/pipeline/lifecycle-pipeline.js +1 -85
  22. package/dist/node/telemetry/model-selection-telemetry.js +1 -30
  23. package/dist/node/telemetry/posthog-baseline-reader.js +5 -308
  24. package/dist/node/telemetry/posthog-telemetry.js +1 -60
  25. package/dist/node/tracing/core.js +1 -52
  26. package/dist/node/tracing/index.js +1 -75
  27. package/dist/node/tracing/middleware.js +1 -171
  28. package/dist/node/tracing/model-selection.span.js +1 -72
  29. package/dist/pipeline/evolution-pipeline.js +1 -299
  30. package/dist/pipeline/lifecycle-pipeline.js +1 -85
  31. package/dist/telemetry/model-selection-telemetry.js +1 -30
  32. package/dist/telemetry/posthog-baseline-reader.js +5 -308
  33. package/dist/telemetry/posthog-telemetry.js +1 -60
  34. package/dist/tracing/core.js +1 -52
  35. package/dist/tracing/index.js +1 -75
  36. package/dist/tracing/middleware.js +1 -171
  37. package/dist/tracing/model-selection.span.js +1 -72
  38. package/package.json +8 -8
@@ -1,1128 +1,5 @@
1
- // src/anomaly/alert-manager.ts
2
- class AlertManager {
3
- options;
4
- cooldownMs;
5
- lastAlert = new Map;
6
- constructor(options) {
7
- this.options = options;
8
- this.cooldownMs = options.cooldownMs ?? 60000;
9
- }
10
- async notify(signal, analysis) {
11
- const key = `${signal.type}:${analysis.culprit?.id ?? "none"}`;
12
- const now = Date.now();
13
- const last = this.lastAlert.get(key) ?? 0;
14
- if (now - last < this.cooldownMs) {
15
- return;
16
- }
17
- await this.options.transport({ signal, analysis });
18
- this.lastAlert.set(key, now);
19
- }
20
- }
21
-
22
- // src/anomaly/baseline-calculator.ts
23
- class BaselineCalculator {
24
- alpha;
25
- snapshot = {
26
- latencyP99: 0,
27
- latencyP95: 0,
28
- errorRate: 0,
29
- throughput: 0,
30
- sampleCount: 0
31
- };
32
- constructor(alpha = 0.2) {
33
- this.alpha = alpha;
34
- }
35
- update(point) {
36
- const { sampleCount } = this.snapshot;
37
- const nextCount = sampleCount + 1;
38
- const weight = sampleCount === 0 ? 1 : this.alpha;
39
- this.snapshot = {
40
- latencyP99: this.mix(this.snapshot.latencyP99, point.latencyP99, weight),
41
- latencyP95: this.mix(this.snapshot.latencyP95, point.latencyP95, weight),
42
- errorRate: this.mix(this.snapshot.errorRate, point.errorRate, weight),
43
- throughput: this.mix(this.snapshot.throughput, point.throughput, weight),
44
- sampleCount: nextCount
45
- };
46
- return this.snapshot;
47
- }
48
- getSnapshot() {
49
- return this.snapshot;
50
- }
51
- mix(current, next, weight) {
52
- if (this.snapshot.sampleCount === 0) {
53
- return next;
54
- }
55
- return current * (1 - weight) + next * weight;
56
- }
57
- }
58
-
59
- // src/anomaly/anomaly-detector.ts
60
- class AnomalyDetector {
61
- baseline;
62
- thresholds = {
63
- errorRateDelta: 0.5,
64
- latencyDelta: 0.35,
65
- throughputDrop: 0.4,
66
- minSamples: 10
67
- };
68
- constructor(options = {}) {
69
- this.baseline = new BaselineCalculator;
70
- this.thresholds = { ...this.thresholds, ...options };
71
- }
72
- evaluate(point) {
73
- const baselineSnapshot = this.baseline.update(point);
74
- if (baselineSnapshot.sampleCount < this.thresholds.minSamples) {
75
- return [];
76
- }
77
- const signals = [];
78
- const errorDelta = this.relativeDelta(point.errorRate, baselineSnapshot.errorRate);
79
- if (errorDelta > this.thresholds.errorRateDelta) {
80
- signals.push({
81
- type: "error_rate_spike",
82
- delta: errorDelta,
83
- point,
84
- baseline: baselineSnapshot
85
- });
86
- }
87
- const latencyDelta = this.relativeDelta(point.latencyP99, baselineSnapshot.latencyP99);
88
- if (latencyDelta > this.thresholds.latencyDelta) {
89
- signals.push({
90
- type: "latency_regression",
91
- delta: latencyDelta,
92
- point,
93
- baseline: baselineSnapshot
94
- });
95
- }
96
- const throughputDelta = this.relativeDrop(point.throughput, baselineSnapshot.throughput);
97
- if (throughputDelta > this.thresholds.throughputDrop) {
98
- signals.push({
99
- type: "throughput_drop",
100
- delta: throughputDelta,
101
- point,
102
- baseline: baselineSnapshot
103
- });
104
- }
105
- return signals;
106
- }
107
- relativeDelta(value, baseline) {
108
- if (baseline === 0) {
109
- return 0;
110
- }
111
- return (value - baseline) / baseline;
112
- }
113
- relativeDrop(value, baseline) {
114
- if (baseline === 0) {
115
- return 0;
116
- }
117
- return (baseline - value) / baseline;
118
- }
119
- }
120
-
121
- // src/anomaly/root-cause-analyzer.ts
122
- class RootCauseAnalyzer {
123
- lookbackMs;
124
- constructor(lookbackMs = 15 * 60 * 1000) {
125
- this.lookbackMs = lookbackMs;
126
- }
127
- analyze(signal, deployments) {
128
- const windowStart = new Date(signal.point.timestamp.getTime() - this.lookbackMs);
129
- const candidates = deployments.filter((deployment) => deployment.deployedAt >= windowStart).sort((a, b) => b.deployedAt.getTime() - a.deployedAt.getTime());
130
- const notes = [];
131
- let culprit;
132
- if (candidates.length > 0) {
133
- culprit = candidates[0];
134
- if (culprit) {
135
- notes.push(`Closest deployment ${culprit.id} (${culprit.operation}) at ${culprit.deployedAt.toISOString()}`);
136
- }
137
- } else {
138
- notes.push("No deployments found within lookback window.");
139
- }
140
- if (signal.type === "latency_regression") {
141
- notes.push("Verify recent schema changes and external dependency latency.");
142
- }
143
- if (signal.type === "error_rate_spike") {
144
- notes.push("Check SLO monitor for correlated incidents.");
145
- }
146
- return { signal, culprit, notes };
147
- }
148
- }
149
-
150
- // src/intent/aggregator.ts
151
- var DEFAULT_WINDOW_MS = 15 * 60 * 1000;
152
-
153
- class IntentAggregator {
154
- windowMs;
155
- sequenceSampleSize;
156
- samples = [];
157
- constructor(options = {}) {
158
- this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
159
- this.sequenceSampleSize = options.sequenceSampleSize ?? 1000;
160
- }
161
- add(sample) {
162
- this.samples.push(sample);
163
- }
164
- flush(now = new Date) {
165
- const minTimestamp = now.getTime() - this.windowMs;
166
- const windowSamples = this.samples.filter((sample) => sample.timestamp.getTime() >= minTimestamp);
167
- this.samples.length = 0;
168
- const metrics = this.aggregateMetrics(windowSamples);
169
- const sequences = this.buildSequences(windowSamples);
170
- const timestamps = windowSamples.map((sample) => sample.timestamp.getTime());
171
- return {
172
- metrics,
173
- sequences,
174
- sampleCount: windowSamples.length,
175
- windowStart: timestamps.length ? new Date(Math.min(...timestamps)) : undefined,
176
- windowEnd: timestamps.length ? new Date(Math.max(...timestamps)) : undefined
177
- };
178
- }
179
- aggregateMetrics(samples) {
180
- if (!samples.length)
181
- return [];
182
- const groups = new Map;
183
- for (const sample of samples) {
184
- const key = `${sample.operation.name}.v${sample.operation.version}`;
185
- const arr = groups.get(key) ?? [];
186
- arr.push(sample);
187
- groups.set(key, arr);
188
- }
189
- return [...groups.values()].map((group) => {
190
- const first = group[0];
191
- if (!first)
192
- throw new Error("Empty group in aggregation");
193
- const durations = group.map((s) => s.durationMs).sort((a, b) => a - b);
194
- const errors = group.filter((s) => !s.success);
195
- const totalCalls = group.length;
196
- const topErrors = errors.reduce((acc, sample) => {
197
- if (!sample.errorCode)
198
- return acc;
199
- acc[sample.errorCode] = (acc[sample.errorCode] ?? 0) + 1;
200
- return acc;
201
- }, {});
202
- const timestamps = group.map((s) => s.timestamp.getTime());
203
- return {
204
- operation: first.operation,
205
- totalCalls,
206
- successRate: (totalCalls - errors.length) / totalCalls,
207
- errorRate: errors.length / totalCalls,
208
- averageLatencyMs: durations.reduce((sum, value) => sum + value, 0) / totalCalls,
209
- p95LatencyMs: percentile(durations, 0.95),
210
- p99LatencyMs: percentile(durations, 0.99),
211
- maxLatencyMs: Math.max(...durations),
212
- windowStart: new Date(Math.min(...timestamps)),
213
- windowEnd: new Date(Math.max(...timestamps)),
214
- topErrors
215
- };
216
- });
217
- }
218
- buildSequences(samples) {
219
- const byTrace = new Map;
220
- for (const sample of samples.slice(-this.sequenceSampleSize)) {
221
- if (!sample.traceId)
222
- continue;
223
- const arr = byTrace.get(sample.traceId) ?? [];
224
- arr.push(sample);
225
- byTrace.set(sample.traceId, arr);
226
- }
227
- const sequences = {};
228
- for (const events of byTrace.values()) {
229
- const ordered = events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
230
- const steps = ordered.map((event) => event.operation.name);
231
- if (steps.length < 2)
232
- continue;
233
- const key = `${steps.join(">")}@${ordered[0]?.tenantId ?? "global"}`;
234
- const existing = sequences[key];
235
- if (existing) {
236
- existing.count += 1;
237
- } else {
238
- sequences[key] = {
239
- steps,
240
- tenantId: ordered[0]?.tenantId,
241
- count: 1
242
- };
243
- }
244
- }
245
- return Object.values(sequences).sort((a, b) => b.count - a.count);
246
- }
247
- }
248
- function percentile(values, ratio) {
249
- if (!values.length)
250
- return 0;
251
- if (values.length === 1)
252
- return values[0] ?? 0;
253
- const index = Math.min(values.length - 1, Math.floor(ratio * values.length));
254
- return values[index] ?? 0;
255
- }
256
-
257
- // src/intent/detector.ts
258
- import { randomUUID } from "crypto";
259
- var DEFAULTS = {
260
- errorRateThreshold: 0.05,
261
- latencyP99ThresholdMs: 750,
262
- throughputDropThreshold: 0.3,
263
- minSequenceLength: 3
264
- };
265
-
266
- class IntentDetector {
267
- options;
268
- constructor(options = {}) {
269
- this.options = {
270
- errorRateThreshold: options.errorRateThreshold ?? DEFAULTS.errorRateThreshold,
271
- latencyP99ThresholdMs: options.latencyP99ThresholdMs ?? DEFAULTS.latencyP99ThresholdMs,
272
- throughputDropThreshold: options.throughputDropThreshold ?? DEFAULTS.throughputDropThreshold,
273
- minSequenceLength: options.minSequenceLength ?? DEFAULTS.minSequenceLength
274
- };
275
- }
276
- detectFromMetrics(current, previous) {
277
- const signals = [];
278
- const baseline = new Map((previous ?? []).map((metric) => [
279
- `${metric.operation.name}.v${metric.operation.version}`,
280
- metric
281
- ]));
282
- for (const metric of current) {
283
- if (metric.errorRate >= this.options.errorRateThreshold) {
284
- signals.push({
285
- id: randomUUID(),
286
- type: "error-spike",
287
- operation: metric.operation,
288
- confidence: Math.min(1, metric.errorRate / this.options.errorRateThreshold),
289
- description: `Error rate ${metric.errorRate.toFixed(2)} exceeded threshold`,
290
- metadata: {
291
- errorRate: metric.errorRate,
292
- topErrors: metric.topErrors
293
- },
294
- evidence: [
295
- {
296
- type: "metric",
297
- description: "error-rate",
298
- data: {
299
- errorRate: metric.errorRate,
300
- threshold: this.options.errorRateThreshold
301
- }
302
- }
303
- ]
304
- });
305
- continue;
306
- }
307
- if (metric.p99LatencyMs >= this.options.latencyP99ThresholdMs) {
308
- signals.push({
309
- id: randomUUID(),
310
- type: "latency-regression",
311
- operation: metric.operation,
312
- confidence: Math.min(1, metric.p99LatencyMs / this.options.latencyP99ThresholdMs),
313
- description: `P99 latency ${metric.p99LatencyMs}ms exceeded threshold`,
314
- metadata: { p99LatencyMs: metric.p99LatencyMs },
315
- evidence: [
316
- {
317
- type: "metric",
318
- description: "p99-latency",
319
- data: {
320
- p99LatencyMs: metric.p99LatencyMs,
321
- threshold: this.options.latencyP99ThresholdMs
322
- }
323
- }
324
- ]
325
- });
326
- continue;
327
- }
328
- const base = baseline.get(`${metric.operation.name}.v${metric.operation.version}`);
329
- if (base) {
330
- const drop = (base.totalCalls - metric.totalCalls) / Math.max(base.totalCalls, 1);
331
- if (drop >= this.options.throughputDropThreshold) {
332
- signals.push({
333
- id: randomUUID(),
334
- type: "throughput-drop",
335
- operation: metric.operation,
336
- confidence: Math.min(1, drop / this.options.throughputDropThreshold),
337
- description: `Throughput dropped ${(drop * 100).toFixed(1)}% vs baseline`,
338
- metadata: {
339
- baselineCalls: base.totalCalls,
340
- currentCalls: metric.totalCalls
341
- },
342
- evidence: [
343
- {
344
- type: "metric",
345
- description: "throughput-drop",
346
- data: {
347
- baselineCalls: base.totalCalls,
348
- currentCalls: metric.totalCalls
349
- }
350
- }
351
- ]
352
- });
353
- }
354
- }
355
- }
356
- return signals;
357
- }
358
- detectSequentialIntents(sequences) {
359
- const signals = [];
360
- for (const sequence of sequences) {
361
- if (sequence.steps.length < this.options.minSequenceLength)
362
- continue;
363
- const description = sequence.steps.join(" → ");
364
- signals.push({
365
- id: randomUUID(),
366
- type: "missing-workflow-step",
367
- confidence: 0.6,
368
- description: `Repeated workflow detected: ${description}`,
369
- metadata: {
370
- steps: sequence.steps,
371
- tenantId: sequence.tenantId,
372
- occurrences: sequence.count
373
- },
374
- evidence: [
375
- {
376
- type: "sequence",
377
- description: "sequential-calls",
378
- data: { steps: sequence.steps, count: sequence.count }
379
- }
380
- ]
381
- });
382
- }
383
- return signals;
384
- }
385
- }
386
-
387
- // src/logging/index.ts
388
- import { context, trace } from "@opentelemetry/api";
389
-
390
- class Logger {
391
- serviceName;
392
- constructor(serviceName) {
393
- this.serviceName = serviceName;
394
- }
395
- log(level, message, meta = {}) {
396
- const span = trace.getSpan(context.active());
397
- const traceId = span?.spanContext().traceId;
398
- const spanId = span?.spanContext().spanId;
399
- const entry = {
400
- timestamp: new Date().toISOString(),
401
- service: this.serviceName,
402
- level,
403
- message,
404
- traceId,
405
- spanId,
406
- ...meta
407
- };
408
- console.log(JSON.stringify(entry));
409
- }
410
- debug(message, meta) {
411
- this.log("debug", message, meta);
412
- }
413
- info(message, meta) {
414
- this.log("info", message, meta);
415
- }
416
- warn(message, meta) {
417
- this.log("warn", message, meta);
418
- }
419
- error(message, meta) {
420
- this.log("error", message, meta);
421
- }
422
- }
423
- var logger = new Logger(process.env.OTEL_SERVICE_NAME || "unknown-service");
424
-
425
- // src/metrics/index.ts
426
- import {
427
- metrics
428
- } from "@opentelemetry/api";
429
- var DEFAULT_METER_NAME = "@contractspec/lib.observability";
430
- function getMeter(name = DEFAULT_METER_NAME) {
431
- return metrics.getMeter(name);
432
- }
433
- function createCounter(name, description, meterName) {
434
- return getMeter(meterName).createCounter(name, { description });
435
- }
436
- function createUpDownCounter(name, description, meterName) {
437
- return getMeter(meterName).createUpDownCounter(name, { description });
438
- }
439
- function createHistogram(name, description, meterName) {
440
- return getMeter(meterName).createHistogram(name, { description });
441
- }
442
- var standardMetrics = {
443
- httpRequests: createCounter("http_requests_total", "Total HTTP requests"),
444
- httpDuration: createHistogram("http_request_duration_seconds", "HTTP request duration"),
445
- operationErrors: createCounter("operation_errors_total", "Total operation errors"),
446
- workflowDuration: createHistogram("workflow_duration_seconds", "Workflow execution duration")
447
- };
448
-
449
- // src/pipeline/evolution-pipeline.ts
450
- import { EventEmitter } from "node:events";
451
- class EvolutionPipeline {
452
- detector;
453
- aggregator;
454
- emitter;
455
- onIntent;
456
- onSnapshot;
457
- timer;
458
- previousMetrics;
459
- constructor(options = {}) {
460
- this.detector = options.detector ?? new IntentDetector;
461
- this.aggregator = options.aggregator ?? new IntentAggregator;
462
- this.emitter = options.emitter ?? new EventEmitter;
463
- this.onIntent = options.onIntent;
464
- this.onSnapshot = options.onSnapshot;
465
- }
466
- ingest(sample) {
467
- this.aggregator.add(sample);
468
- }
469
- on(listener) {
470
- this.emitter.on("event", listener);
471
- }
472
- start(intervalMs = 5 * 60 * 1000) {
473
- this.stop();
474
- this.timer = setInterval(() => {
475
- this.run();
476
- }, intervalMs);
477
- }
478
- stop() {
479
- if (this.timer) {
480
- clearInterval(this.timer);
481
- this.timer = undefined;
482
- }
483
- }
484
- async run() {
485
- const snapshot = this.aggregator.flush();
486
- this.emit({
487
- type: "telemetry.window",
488
- payload: { sampleCount: snapshot.sampleCount }
489
- });
490
- if (this.onSnapshot)
491
- await this.onSnapshot(snapshot);
492
- if (!snapshot.sampleCount)
493
- return;
494
- const metricSignals = this.detector.detectFromMetrics(snapshot.metrics, this.previousMetrics);
495
- const sequenceSignals = this.detector.detectSequentialIntents(snapshot.sequences);
496
- this.previousMetrics = snapshot.metrics;
497
- const signals = [...metricSignals, ...sequenceSignals];
498
- for (const signal of signals) {
499
- if (this.onIntent)
500
- await this.onIntent(signal);
501
- this.emit({ type: "intent.detected", payload: signal });
502
- }
503
- }
504
- emit(event) {
505
- this.emitter.emit("event", event);
506
- }
507
- }
508
-
509
- // src/pipeline/lifecycle-pipeline.ts
510
- import { EventEmitter as EventEmitter2 } from "node:events";
511
- import { getStageLabel } from "@contractspec/lib.lifecycle";
512
- class LifecycleKpiPipeline {
513
- assessmentCounter;
514
- confidenceHistogram;
515
- stageUpDownCounter;
516
- emitter;
517
- lowConfidenceThreshold;
518
- currentStageByTenant = new Map;
519
- constructor(options = {}) {
520
- const meterName = options.meterName ?? "@contractspec/lib.lifecycle-kpi";
521
- this.assessmentCounter = createCounter("lifecycle_assessments_total", "Total lifecycle assessments", meterName);
522
- this.confidenceHistogram = createHistogram("lifecycle_assessment_confidence", "Lifecycle assessment confidence distribution", meterName);
523
- this.stageUpDownCounter = createUpDownCounter("lifecycle_stage_tenants", "Current tenants per lifecycle stage", meterName);
524
- this.emitter = options.emitter ?? new EventEmitter2;
525
- this.lowConfidenceThreshold = options.lowConfidenceThreshold ?? 0.4;
526
- }
527
- recordAssessment(assessment, tenantId) {
528
- const stageLabel = getStageLabel(assessment.stage);
529
- const attributes = { stage: stageLabel, tenantId };
530
- this.assessmentCounter.add(1, attributes);
531
- this.confidenceHistogram.record(assessment.confidence, attributes);
532
- this.ensureStageCounters(assessment.stage, tenantId);
533
- this.emitter.emit("event", {
534
- type: "assessment.recorded",
535
- payload: { tenantId, stage: assessment.stage }
536
- });
537
- if (assessment.confidence < this.lowConfidenceThreshold) {
538
- this.emitter.emit("event", {
539
- type: "confidence.low",
540
- payload: { tenantId, confidence: assessment.confidence }
541
- });
542
- }
543
- }
544
- on(listener) {
545
- this.emitter.on("event", listener);
546
- }
547
- ensureStageCounters(stage, tenantId) {
548
- if (!tenantId)
549
- return;
550
- const previous = this.currentStageByTenant.get(tenantId);
551
- if (previous === stage)
552
- return;
553
- if (previous !== undefined) {
554
- this.stageUpDownCounter.add(-1, {
555
- stage: getStageLabel(previous),
556
- tenantId
557
- });
558
- }
559
- this.stageUpDownCounter.add(1, { stage: getStageLabel(stage), tenantId });
560
- this.currentStageByTenant.set(tenantId, stage);
561
- this.emitter.emit("event", {
562
- type: "stage.changed",
563
- payload: { tenantId, previousStage: previous, nextStage: stage }
564
- });
565
- }
566
- }
567
-
568
- // src/telemetry/model-selection-telemetry.ts
569
- class ModelSelectionTelemetry {
570
- provider;
571
- eventName;
572
- constructor(provider, options) {
573
- this.provider = provider;
574
- this.eventName = options?.eventName ?? "$model_selection";
575
- }
576
- async trackSelection(distinctId, properties) {
577
- await this.provider.capture({
578
- distinctId,
579
- event: this.eventName,
580
- timestamp: new Date,
581
- properties: {
582
- $model_id: properties.modelId,
583
- $model_provider: properties.providerKey,
584
- $model_score: properties.score,
585
- $model_dimension: properties.dimension ?? null,
586
- $model_reason: properties.reason,
587
- $model_alternatives_count: properties.alternativesCount,
588
- $model_cost_estimate_input: properties.costEstimateInput ?? null,
589
- $model_cost_estimate_output: properties.costEstimateOutput ?? null,
590
- $model_selection_duration_ms: properties.selectionDurationMs ?? null
591
- }
592
- });
593
- }
594
- }
595
-
596
- // src/telemetry/posthog-baseline-reader.ts
597
- class PosthogBaselineReader {
598
- reader;
599
- eventPrefix;
600
- constructor(reader, options = {}) {
601
- this.reader = reader;
602
- this.eventPrefix = options.eventPrefix ?? "observability";
603
- }
604
- async readSamples(input) {
605
- const result = await this.queryHogQL({
606
- query: [
607
- "select",
608
- " properties.operation as operationName,",
609
- " properties.version as version,",
610
- " properties.durationMs as durationMs,",
611
- " properties.success as success,",
612
- " properties.errorCode as errorCode,",
613
- " properties.tenantId as tenantId,",
614
- " properties.traceId as traceId,",
615
- " properties.metadata as metadata,",
616
- " distinct_id as actorId,",
617
- " timestamp as timestamp",
618
- "from events",
619
- `where ${buildOperationWhereClause(this.eventPrefix, input)}`,
620
- "order by timestamp desc",
621
- `limit ${input.limit ?? 1000}`
622
- ].join(`
623
- `),
624
- values: buildOperationValues(input)
625
- });
626
- return mapTelemetrySamples(result);
627
- }
628
- async readAggregatedMetrics(operation, windowDays = 7) {
629
- const dateRange = buildWindowRange(windowDays);
630
- const result = await this.queryHogQL({
631
- query: [
632
- "select",
633
- " count() as totalCalls,",
634
- " avg(properties.durationMs) as averageLatencyMs,",
635
- " quantile(0.95)(properties.durationMs) as p95LatencyMs,",
636
- " quantile(0.99)(properties.durationMs) as p99LatencyMs,",
637
- " max(properties.durationMs) as maxLatencyMs,",
638
- " sum(if(properties.success = 1, 1, 0)) as successCount,",
639
- " sum(if(properties.success = 0, 1, 0)) as errorCount",
640
- "from events",
641
- `where ${buildOperationWhereClause(this.eventPrefix, {
642
- operations: [operation],
643
- dateRange
644
- })}`
645
- ].join(`
646
- `),
647
- values: buildOperationValues({
648
- operations: [operation],
649
- dateRange
650
- })
651
- });
652
- const stats = mapAggregatedMetrics(result, operation, dateRange);
653
- if (!stats)
654
- return null;
655
- const topErrors = await this.readTopErrors(operation, dateRange);
656
- return {
657
- ...stats,
658
- topErrors
659
- };
660
- }
661
- async readOperationSequences(dateRange) {
662
- const result = await this.queryHogQL({
663
- query: [
664
- "select",
665
- " properties.sequences as sequences",
666
- "from events",
667
- `where event = {eventName}`,
668
- dateRange?.from ? "and timestamp >= {dateFrom}" : "",
669
- dateRange?.to ? "and timestamp < {dateTo}" : "",
670
- "order by timestamp desc",
671
- "limit 50"
672
- ].filter(Boolean).join(`
673
- `),
674
- values: {
675
- eventName: `${this.eventPrefix}.window`,
676
- dateFrom: toIsoString(dateRange?.from),
677
- dateTo: toIsoString(dateRange?.to)
678
- }
679
- });
680
- return mergeSequences(result);
681
- }
682
- async readTopErrors(operation, dateRange) {
683
- const result = await this.queryHogQL({
684
- query: [
685
- "select",
686
- " properties.errorCode as errorCode,",
687
- " count() as errorCount",
688
- "from events",
689
- `where ${buildOperationWhereClause(this.eventPrefix, {
690
- operations: [operation],
691
- dateRange
692
- })} and properties.success = 0`,
693
- "group by errorCode",
694
- "order by errorCount desc",
695
- "limit 5"
696
- ].join(`
697
- `),
698
- values: buildOperationValues({
699
- operations: [operation],
700
- dateRange
701
- })
702
- });
703
- const rows = mapRows(result);
704
- return rows.reduce((acc, row) => {
705
- const code = asString(row.errorCode);
706
- if (!code)
707
- return acc;
708
- acc[code] = asNumber(row.errorCount);
709
- return acc;
710
- }, {});
711
- }
712
- async queryHogQL(input) {
713
- if (!this.reader.queryHogQL) {
714
- throw new Error("Analytics reader does not support HogQL queries.");
715
- }
716
- return this.reader.queryHogQL(input);
717
- }
718
- }
719
- function buildOperationWhereClause(eventPrefix, input) {
720
- const clauses = [`event = '${eventPrefix}.operation'`];
721
- if (input.operations?.length) {
722
- clauses.push(`(${buildOperationFilters(input.operations)})`);
723
- }
724
- if (input.dateRange?.from) {
725
- clauses.push("timestamp >= {dateFrom}");
726
- }
727
- if (input.dateRange?.to) {
728
- clauses.push("timestamp < {dateTo}");
729
- }
730
- return clauses.join(" and ");
731
- }
732
- function buildOperationValues(input) {
733
- const values = {
734
- dateFrom: toIsoString(input.dateRange?.from),
735
- dateTo: toIsoString(input.dateRange?.to)
736
- };
737
- input.operations?.forEach((op, index) => {
738
- values[`operationName${index}`] = op.name;
739
- values[`operationVersion${index}`] = op.version;
740
- });
741
- return values;
742
- }
743
- function buildOperationFilters(operations) {
744
- return operations.map((_op, index) => `(properties.operation = {operationName${index}} and properties.version = {operationVersion${index}})`).join(" or ");
745
- }
746
- function mapTelemetrySamples(result) {
747
- const rows = mapRows(result);
748
- return rows.flatMap((row) => {
749
- const operationName = asString(row.operationName);
750
- const version = asString(row.version);
751
- const timestamp = asDate(row.timestamp);
752
- if (!operationName || !version || !timestamp) {
753
- return [];
754
- }
755
- return [
756
- {
757
- operation: { name: operationName, version },
758
- durationMs: asNumber(row.durationMs),
759
- success: asBoolean(row.success),
760
- timestamp,
761
- errorCode: asOptionalString(row.errorCode) ?? undefined,
762
- tenantId: asOptionalString(row.tenantId) ?? undefined,
763
- traceId: asOptionalString(row.traceId) ?? undefined,
764
- actorId: asOptionalString(row.actorId) ?? undefined,
765
- metadata: isRecord(row.metadata) ? row.metadata : undefined
766
- }
767
- ];
768
- });
769
- }
770
- function mapAggregatedMetrics(result, operation, dateRange) {
771
- const rows = mapRows(result);
772
- const row = rows[0];
773
- if (!row)
774
- return null;
775
- const totalCalls = asNumber(row.totalCalls);
776
- if (!totalCalls)
777
- return null;
778
- const successCount = asNumber(row.successCount);
779
- const errorCount = asNumber(row.errorCount);
780
- const windowStart = toDate(dateRange.from) ?? new Date;
781
- const windowEnd = toDate(dateRange.to) ?? new Date;
782
- return {
783
- operation,
784
- totalCalls,
785
- successRate: totalCalls ? successCount / totalCalls : 0,
786
- errorRate: totalCalls ? errorCount / totalCalls : 0,
787
- averageLatencyMs: asNumber(row.averageLatencyMs),
788
- p95LatencyMs: asNumber(row.p95LatencyMs),
789
- p99LatencyMs: asNumber(row.p99LatencyMs),
790
- maxLatencyMs: asNumber(row.maxLatencyMs),
791
- windowStart,
792
- windowEnd,
793
- topErrors: {}
794
- };
795
- }
796
- function mergeSequences(result) {
797
- const rows = mapRows(result);
798
- const merged = new Map;
799
- rows.forEach((row) => {
800
- const sequences = row.sequences;
801
- if (!Array.isArray(sequences))
802
- return;
803
- sequences.forEach((sequence) => {
804
- if (!isRecord(sequence))
805
- return;
806
- const steps = Array.isArray(sequence.steps) ? sequence.steps.filter((step) => typeof step === "string") : [];
807
- if (steps.length === 0)
808
- return;
809
- const tenantId = typeof sequence.tenantId === "string" ? sequence.tenantId : undefined;
810
- const count = typeof sequence.count === "number" && Number.isFinite(sequence.count) ? sequence.count : 0;
811
- const key = `${tenantId ?? "global"}:${steps.join(">")}`;
812
- const existing = merged.get(key);
813
- if (existing) {
814
- existing.count += count;
815
- } else {
816
- merged.set(key, { steps, tenantId, count });
817
- }
818
- });
819
- });
820
- return [...merged.values()];
821
- }
822
- function mapRows(result) {
823
- if (!Array.isArray(result.results) || !Array.isArray(result.columns)) {
824
- return [];
825
- }
826
- const columns = result.columns;
827
- return result.results.flatMap((row) => {
828
- if (!Array.isArray(row))
829
- return [];
830
- const record = {};
831
- columns.forEach((column, index) => {
832
- record[column] = row[index];
833
- });
834
- return [record];
835
- });
836
- }
837
- function buildWindowRange(windowDays) {
838
- const windowEnd = new Date;
839
- const windowStart = new Date(windowEnd.getTime() - windowDays * 24 * 60 * 60 * 1000);
840
- return {
841
- from: windowStart,
842
- to: windowEnd
843
- };
844
- }
845
- function asString(value) {
846
- if (typeof value === "string" && value.trim())
847
- return value;
848
- if (typeof value === "number")
849
- return String(value);
850
- return null;
851
- }
852
- function asOptionalString(value) {
853
- if (typeof value === "string")
854
- return value;
855
- if (typeof value === "number")
856
- return String(value);
857
- return null;
858
- }
859
- function asNumber(value) {
860
- if (typeof value === "number" && Number.isFinite(value))
861
- return value;
862
- if (typeof value === "string" && value.trim()) {
863
- const parsed = Number(value);
864
- if (Number.isFinite(parsed))
865
- return parsed;
866
- }
867
- return 0;
868
- }
869
- function asBoolean(value) {
870
- if (typeof value === "boolean")
871
- return value;
872
- if (typeof value === "number")
873
- return value !== 0;
874
- if (typeof value === "string")
875
- return value.toLowerCase() === "true";
876
- return false;
877
- }
878
- function asDate(value) {
879
- if (value instanceof Date)
880
- return value;
881
- if (typeof value === "string" || typeof value === "number") {
882
- const date = new Date(value);
883
- if (!Number.isNaN(date.getTime()))
884
- return date;
885
- }
886
- return null;
887
- }
888
- function toIsoString(value) {
889
- if (!value)
890
- return;
891
- return typeof value === "string" ? value : value.toISOString();
892
- }
893
- function toDate(value) {
894
- if (!value)
895
- return null;
896
- return value instanceof Date ? value : new Date(value);
897
- }
898
- function isRecord(value) {
899
- return typeof value === "object" && value !== null;
900
- }
901
-
902
- // src/telemetry/posthog-telemetry.ts
903
- class PosthogTelemetryProvider {
904
- provider;
905
- eventPrefix;
906
- includeMetadata;
907
- constructor(provider, options = {}) {
908
- this.provider = provider;
909
- this.eventPrefix = options.eventPrefix ?? "observability";
910
- this.includeMetadata = options.includeMetadata ?? false;
911
- }
912
- async captureSample(sample) {
913
- await this.provider.capture({
914
- distinctId: sample.actorId ?? sample.tenantId ?? "unknown",
915
- event: `${this.eventPrefix}.operation`,
916
- timestamp: sample.timestamp,
917
- properties: {
918
- operation: sample.operation.name,
919
- version: sample.operation.version,
920
- durationMs: sample.durationMs,
921
- success: sample.success,
922
- errorCode: sample.errorCode ?? null,
923
- tenantId: sample.tenantId ?? null,
924
- traceId: sample.traceId ?? null,
925
- ...this.includeMetadata && sample.metadata ? { metadata: sample.metadata } : {}
926
- }
927
- });
928
- }
929
- async captureSnapshot(snapshot) {
930
- await this.provider.capture({
931
- distinctId: "system",
932
- event: `${this.eventPrefix}.window`,
933
- timestamp: snapshot.windowEnd ?? new Date,
934
- properties: {
935
- sampleCount: snapshot.sampleCount,
936
- metricsCount: snapshot.metrics.length,
937
- sequencesCount: snapshot.sequences.length,
938
- windowStart: snapshot.windowStart?.toISOString() ?? null,
939
- windowEnd: snapshot.windowEnd?.toISOString() ?? null,
940
- ...this.includeMetadata ? {
941
- metrics: snapshot.metrics.map((metric) => ({
942
- operation: metric.operation.name,
943
- version: metric.operation.version,
944
- totalCalls: metric.totalCalls,
945
- successRate: metric.successRate,
946
- errorRate: metric.errorRate,
947
- averageLatencyMs: metric.averageLatencyMs,
948
- p95LatencyMs: metric.p95LatencyMs,
949
- p99LatencyMs: metric.p99LatencyMs,
950
- maxLatencyMs: metric.maxLatencyMs,
951
- topErrors: metric.topErrors
952
- })),
953
- sequences: snapshot.sequences
954
- } : {}
955
- }
956
- });
957
- }
958
- }
959
-
960
- // src/tracing/core.ts
961
- import {
962
- SpanStatusCode,
963
- trace as trace2
964
- } from "@opentelemetry/api";
965
- var DEFAULT_TRACER_NAME = "@contractspec/lib.observability";
966
- function getTracer(name = DEFAULT_TRACER_NAME) {
967
- return trace2.getTracer(name);
968
- }
969
- async function traceAsync(name, fn, tracerName) {
970
- const tracer = getTracer(tracerName);
971
- return tracer.startActiveSpan(name, async (span) => {
972
- try {
973
- const result = await fn(span);
974
- span.setStatus({ code: SpanStatusCode.OK });
975
- return result;
976
- } catch (error) {
977
- span.recordException(error);
978
- span.setStatus({
979
- code: SpanStatusCode.ERROR,
980
- message: error instanceof Error ? error.message : String(error)
981
- });
982
- throw error;
983
- } finally {
984
- span.end();
985
- }
986
- });
987
- }
988
- function traceSync(name, fn, tracerName) {
989
- const tracer = getTracer(tracerName);
990
- return tracer.startActiveSpan(name, (span) => {
991
- try {
992
- const result = fn(span);
993
- span.setStatus({ code: SpanStatusCode.OK });
994
- return result;
995
- } catch (error) {
996
- span.recordException(error);
997
- span.setStatus({
998
- code: SpanStatusCode.ERROR,
999
- message: error instanceof Error ? error.message : String(error)
1000
- });
1001
- throw error;
1002
- } finally {
1003
- span.end();
1004
- }
1005
- });
1006
- }
1007
-
1008
- // src/tracing/model-selection.span.ts
1009
- async function traceModelSelection(fn, input) {
1010
- const startMs = performance.now();
1011
- return traceAsync("model.selection", async (span) => {
1012
- const result = await fn();
1013
- const durationMs = performance.now() - startMs;
1014
- span.setAttribute("model.selected", input.modelId);
1015
- span.setAttribute("model.provider", input.providerKey);
1016
- span.setAttribute("model.score", input.score);
1017
- span.setAttribute("model.alternatives_count", input.alternativesCount);
1018
- span.setAttribute("model.selection_duration_ms", durationMs);
1019
- span.setAttribute("model.reason", input.reason);
1020
- if (input.dimension) {
1021
- span.setAttribute("model.dimension", input.dimension);
1022
- }
1023
- if (input.constraints) {
1024
- span.setAttribute("model.constraints", JSON.stringify(input.constraints));
1025
- }
1026
- return result;
1027
- });
1028
- }
1029
- // src/tracing/middleware.ts
1030
- function createTracingMiddleware(options = {}) {
1031
- return async (req, next) => {
1032
- const method = req.method;
1033
- const url = new URL(req.url);
1034
- const path = url.pathname;
1035
- standardMetrics.httpRequests.add(1, { method, path });
1036
- const startTime = performance.now();
1037
- return traceAsync(`HTTP ${method} ${path}`, async (span) => {
1038
- span.setAttribute("http.method", method);
1039
- span.setAttribute("http.url", req.url);
1040
- try {
1041
- const response = await next();
1042
- span.setAttribute("http.status_code", response.status);
1043
- const duration = (performance.now() - startTime) / 1000;
1044
- standardMetrics.httpDuration.record(duration, {
1045
- method,
1046
- path,
1047
- status: response.status.toString()
1048
- });
1049
- emitTelemetrySample({
1050
- req,
1051
- res: response,
1052
- span,
1053
- success: true,
1054
- durationMs: duration * 1000,
1055
- options
1056
- });
1057
- return response;
1058
- } catch (error) {
1059
- standardMetrics.operationErrors.add(1, { method, path });
1060
- emitTelemetrySample({
1061
- req,
1062
- span,
1063
- success: false,
1064
- durationMs: performance.now() - startTime,
1065
- error,
1066
- options
1067
- });
1068
- throw error;
1069
- }
1070
- });
1071
- };
1072
- }
1073
- function emitTelemetrySample({
1074
- req,
1075
- res,
1076
- span,
1077
- success,
1078
- durationMs,
1079
- error,
1080
- options
1081
- }) {
1082
- if (!options.onSample || !options.resolveOperation)
1083
- return;
1084
- const operation = options.resolveOperation({ req, res });
1085
- if (!operation)
1086
- return;
1087
- const sample = {
1088
- operation,
1089
- durationMs,
1090
- success,
1091
- timestamp: new Date,
1092
- errorCode: !success && error instanceof Error ? error.name : success ? undefined : "unknown",
1093
- tenantId: options.tenantResolver?.(req),
1094
- actorId: options.actorResolver?.(req),
1095
- traceId: span.spanContext().traceId,
1096
- metadata: {
1097
- method: req.method,
1098
- path: new URL(req.url).pathname,
1099
- status: res?.status
1100
- }
1101
- };
1102
- options.onSample(sample);
1103
- }
1104
- export {
1105
- traceSync,
1106
- traceModelSelection,
1107
- traceAsync,
1108
- standardMetrics,
1109
- logger,
1110
- getTracer,
1111
- getMeter,
1112
- createUpDownCounter,
1113
- createTracingMiddleware,
1114
- createHistogram,
1115
- createCounter,
1116
- RootCauseAnalyzer,
1117
- PosthogTelemetryProvider,
1118
- PosthogBaselineReader,
1119
- ModelSelectionTelemetry,
1120
- Logger,
1121
- LifecycleKpiPipeline,
1122
- IntentDetector,
1123
- IntentAggregator,
1124
- EvolutionPipeline,
1125
- BaselineCalculator,
1126
- AnomalyDetector,
1127
- AlertManager
1128
- };
1
+ class q{options;cooldownMs;lastAlert=new Map;constructor(e){this.options=e;this.cooldownMs=e.cooldownMs??60000}async notify(e,t){let r=`${e.type}:${t.culprit?.id??"none"}`,s=Date.now(),n=this.lastAlert.get(r)??0;if(s-n<this.cooldownMs)return;await this.options.transport({signal:e,analysis:t}),this.lastAlert.set(r,s)}}class y{alpha;snapshot={latencyP99:0,latencyP95:0,errorRate:0,throughput:0,sampleCount:0};constructor(e=0.2){this.alpha=e}update(e){let{sampleCount:t}=this.snapshot,r=t+1,s=t===0?1:this.alpha;return this.snapshot={latencyP99:this.mix(this.snapshot.latencyP99,e.latencyP99,s),latencyP95:this.mix(this.snapshot.latencyP95,e.latencyP95,s),errorRate:this.mix(this.snapshot.errorRate,e.errorRate,s),throughput:this.mix(this.snapshot.throughput,e.throughput,s),sampleCount:r},this.snapshot}getSnapshot(){return this.snapshot}mix(e,t,r){if(this.snapshot.sampleCount===0)return t;return e*(1-r)+t*r}}class O{baseline;thresholds={errorRateDelta:0.5,latencyDelta:0.35,throughputDrop:0.4,minSamples:10};constructor(e={}){this.baseline=new y,this.thresholds={...this.thresholds,...e}}evaluate(e){let t=this.baseline.update(e);if(t.sampleCount<this.thresholds.minSamples)return[];let r=[],s=this.relativeDelta(e.errorRate,t.errorRate);if(s>this.thresholds.errorRateDelta)r.push({type:"error_rate_spike",delta:s,point:e,baseline:t});let n=this.relativeDelta(e.latencyP99,t.latencyP99);if(n>this.thresholds.latencyDelta)r.push({type:"latency_regression",delta:n,point:e,baseline:t});let o=this.relativeDrop(e.throughput,t.throughput);if(o>this.thresholds.throughputDrop)r.push({type:"throughput_drop",delta:o,point:e,baseline:t});return r}relativeDelta(e,t){if(t===0)return 0;return(e-t)/t}relativeDrop(e,t){if(t===0)return 0;return(t-e)/t}}class k{lookbackMs;constructor(e=900000){this.lookbackMs=e}analyze(e,t){let r=new Date(e.point.timestamp.getTime()-this.lookbackMs),s=t.filter((a)=>a.deployedAt>=r).sort((a,l)=>l.deployedAt.getTime()-a.deployedAt.getTime()),n=[],o;if(s.length>0){if(o=s[0],o)n.push(`Closest deployment ${o.id} (${o.operation}) at ${o.deployedAt.toISOString()}`)}else n.push("No deployments found within lookback window.");if(e.type==="latency_regression")n.push("Verify recent schema changes and external dependency latency.");if(e.type==="error_rate_spike")n.push("Check SLO monitor for correlated incidents.");return{signal:e,culprit:o,notes:n}}}class f{windowMs;sequenceSampleSize;samples=[];constructor(e={}){this.windowMs=e.windowMs??900000,this.sequenceSampleSize=e.sequenceSampleSize??1000}add(e){this.samples.push(e)}flush(e=new Date){let t=e.getTime()-this.windowMs,r=this.samples.filter((a)=>a.timestamp.getTime()>=t);this.samples.length=0;let s=this.aggregateMetrics(r),n=this.buildSequences(r),o=r.map((a)=>a.timestamp.getTime());return{metrics:s,sequences:n,sampleCount:r.length,windowStart:o.length?new Date(Math.min(...o)):void 0,windowEnd:o.length?new Date(Math.max(...o)):void 0}}aggregateMetrics(e){if(!e.length)return[];let t=new Map;for(let r of e){let s=`${r.operation.name}.v${r.operation.version}`,n=t.get(s)??[];n.push(r),t.set(s,n)}return[...t.values()].map((r)=>{let s=r[0];if(!s)throw Error("Empty group in aggregation");let n=r.map((i)=>i.durationMs).sort((i,p)=>i-p),o=r.filter((i)=>!i.success),a=r.length,l=o.reduce((i,p)=>{if(!p.errorCode)return i;return i[p.errorCode]=(i[p.errorCode]??0)+1,i},{}),c=r.map((i)=>i.timestamp.getTime());return{operation:s.operation,totalCalls:a,successRate:(a-o.length)/a,errorRate:o.length/a,averageLatencyMs:n.reduce((i,p)=>i+p,0)/a,p95LatencyMs:_(n,0.95),p99LatencyMs:_(n,0.99),maxLatencyMs:Math.max(...n),windowStart:new Date(Math.min(...c)),windowEnd:new Date(Math.max(...c)),topErrors:l}})}buildSequences(e){let t=new Map;for(let s of e.slice(-this.sequenceSampleSize)){if(!s.traceId)continue;let n=t.get(s.traceId)??[];n.push(s),t.set(s.traceId,n)}let r={};for(let s of t.values()){let n=s.sort((c,i)=>c.timestamp.getTime()-i.timestamp.getTime()),o=n.map((c)=>c.operation.name);if(o.length<2)continue;let a=`${o.join(">")}@${n[0]?.tenantId??"global"}`,l=r[a];if(l)l.count+=1;else r[a]={steps:o,tenantId:n[0]?.tenantId,count:1}}return Object.values(r).sort((s,n)=>n.count-s.count)}}function _(e,t){if(!e.length)return 0;if(e.length===1)return e[0]??0;let r=Math.min(e.length-1,Math.floor(t*e.length));return e[r]??0}import{randomUUID as w}from"crypto";var v={errorRateThreshold:0.05,latencyP99ThresholdMs:750,throughputDropThreshold:0.3,minSequenceLength:3};class S{options;constructor(e={}){this.options={errorRateThreshold:e.errorRateThreshold??v.errorRateThreshold,latencyP99ThresholdMs:e.latencyP99ThresholdMs??v.latencyP99ThresholdMs,throughputDropThreshold:e.throughputDropThreshold??v.throughputDropThreshold,minSequenceLength:e.minSequenceLength??v.minSequenceLength}}detectFromMetrics(e,t){let r=[],s=new Map((t??[]).map((n)=>[`${n.operation.name}.v${n.operation.version}`,n]));for(let n of e){if(n.errorRate>=this.options.errorRateThreshold){r.push({id:w(),type:"error-spike",operation:n.operation,confidence:Math.min(1,n.errorRate/this.options.errorRateThreshold),description:`Error rate ${n.errorRate.toFixed(2)} exceeded threshold`,metadata:{errorRate:n.errorRate,topErrors:n.topErrors},evidence:[{type:"metric",description:"error-rate",data:{errorRate:n.errorRate,threshold:this.options.errorRateThreshold}}]});continue}if(n.p99LatencyMs>=this.options.latencyP99ThresholdMs){r.push({id:w(),type:"latency-regression",operation:n.operation,confidence:Math.min(1,n.p99LatencyMs/this.options.latencyP99ThresholdMs),description:`P99 latency ${n.p99LatencyMs}ms exceeded threshold`,metadata:{p99LatencyMs:n.p99LatencyMs},evidence:[{type:"metric",description:"p99-latency",data:{p99LatencyMs:n.p99LatencyMs,threshold:this.options.latencyP99ThresholdMs}}]});continue}let o=s.get(`${n.operation.name}.v${n.operation.version}`);if(o){let a=(o.totalCalls-n.totalCalls)/Math.max(o.totalCalls,1);if(a>=this.options.throughputDropThreshold)r.push({id:w(),type:"throughput-drop",operation:n.operation,confidence:Math.min(1,a/this.options.throughputDropThreshold),description:`Throughput dropped ${(a*100).toFixed(1)}% vs baseline`,metadata:{baselineCalls:o.totalCalls,currentCalls:n.totalCalls},evidence:[{type:"metric",description:"throughput-drop",data:{baselineCalls:o.totalCalls,currentCalls:n.totalCalls}}]})}}return r}detectSequentialIntents(e){let t=[];for(let r of e){if(r.steps.length<this.options.minSequenceLength)continue;let s=r.steps.join(" → ");t.push({id:w(),type:"missing-workflow-step",confidence:0.6,description:`Repeated workflow detected: ${s}`,metadata:{steps:r.steps,tenantId:r.tenantId,occurrences:r.count},evidence:[{type:"sequence",description:"sequential-calls",data:{steps:r.steps,count:r.count}}]})}return t}}import{context as K,trace as V}from"@opentelemetry/api";class I{serviceName;constructor(e){this.serviceName=e}log(e,t,r={}){let s=V.getSpan(K.active()),n=s?.spanContext().traceId,o=s?.spanContext().spanId,a={timestamp:new Date().toISOString(),service:this.serviceName,level:e,message:t,traceId:n,spanId:o,...r};console.log(JSON.stringify(a))}debug(e,t){this.log("debug",e,t)}info(e,t){this.log("info",e,t)}warn(e,t){this.log("warn",e,t)}error(e,t){this.log("error",e,t)}}var J=new I(process.env.OTEL_SERVICE_NAME||"unknown-service");import{metrics as G}from"@opentelemetry/api";var X="@contractspec/lib.observability";function M(e=X){return G.getMeter(e)}function m(e,t,r){return M(r).createCounter(e,{description:t})}function D(e,t,r){return M(r).createUpDownCounter(e,{description:t})}function g(e,t,r){return M(r).createHistogram(e,{description:t})}var h={httpRequests:m("http_requests_total","Total HTTP requests"),httpDuration:g("http_request_duration_seconds","HTTP request duration"),operationErrors:m("operation_errors_total","Total operation errors"),workflowDuration:g("workflow_duration_seconds","Workflow execution duration")};import{EventEmitter as Y}from"node:events";class ${detector;aggregator;emitter;onIntent;onSnapshot;timer;previousMetrics;constructor(e={}){this.detector=e.detector??new S,this.aggregator=e.aggregator??new f,this.emitter=e.emitter??new Y,this.onIntent=e.onIntent,this.onSnapshot=e.onSnapshot}ingest(e){this.aggregator.add(e)}on(e){this.emitter.on("event",e)}start(e=300000){this.stop(),this.timer=setInterval(()=>{this.run()},e)}stop(){if(this.timer)clearInterval(this.timer),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);if(!e.sampleCount)return;let t=this.detector.detectFromMetrics(e.metrics,this.previousMetrics),r=this.detector.detectSequentialIntents(e.sequences);this.previousMetrics=e.metrics;let s=[...t,...r];for(let n of s){if(this.onIntent)await this.onIntent(n);this.emit({type:"intent.detected",payload:n})}}emit(e){this.emitter.emit("event",e)}}import{EventEmitter as Z}from"node:events";import{getStageLabel as x}from"@contractspec/lib.lifecycle";class N{assessmentCounter;confidenceHistogram;stageUpDownCounter;emitter;lowConfidenceThreshold;currentStageByTenant=new Map;constructor(e={}){let t=e.meterName??"@contractspec/lib.lifecycle-kpi";this.assessmentCounter=m("lifecycle_assessments_total","Total lifecycle assessments",t),this.confidenceHistogram=g("lifecycle_assessment_confidence","Lifecycle assessment confidence distribution",t),this.stageUpDownCounter=D("lifecycle_stage_tenants","Current tenants per lifecycle stage",t),this.emitter=e.emitter??new Z,this.lowConfidenceThreshold=e.lowConfidenceThreshold??0.4}recordAssessment(e,t){let s={stage:x(e.stage),tenantId:t};if(this.assessmentCounter.add(1,s),this.confidenceHistogram.record(e.confidence,s),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 r=this.currentStageByTenant.get(t);if(r===e)return;if(r!==void 0)this.stageUpDownCounter.add(-1,{stage:x(r),tenantId:t});this.stageUpDownCounter.add(1,{stage:x(e),tenantId:t}),this.currentStageByTenant.set(t,e),this.emitter.emit("event",{type:"stage.changed",payload:{tenantId:t,previousStage:r,nextStage:e}})}}class H{provider;eventName;constructor(e,t){this.provider=e,this.eventName=t?.eventName??"$model_selection"}async trackSelection(e,t){await this.provider.capture({distinctId:e,event:this.eventName,timestamp:new Date,properties:{$model_id:t.modelId,$model_provider:t.providerKey,$model_score:t.score,$model_dimension:t.dimension??null,$model_reason:t.reason,$model_alternatives_count:t.alternativesCount,$model_cost_estimate_input:t.costEstimateInput??null,$model_cost_estimate_output:t.costEstimateOutput??null,$model_selection_duration_ms:t.selectionDurationMs??null}})}}class F{reader;eventPrefix;constructor(e,t={}){this.reader=e,this.eventPrefix=t.eventPrefix??"observability"}async readSamples(e){let t=await this.queryHogQL({query:["select"," properties.operation as operationName,"," properties.version as version,"," properties.durationMs as durationMs,"," properties.success as success,"," properties.errorCode as errorCode,"," properties.tenantId as tenantId,"," properties.traceId as traceId,"," properties.metadata as metadata,"," distinct_id as actorId,"," timestamp as timestamp","from events",`where ${L(this.eventPrefix,e)}`,"order by timestamp desc",`limit ${e.limit??1000}`].join(`
2
+ `),values:E(e)});return te(t)}async readAggregatedMetrics(e,t=7){let r=oe(t),s=await this.queryHogQL({query:["select"," count() as totalCalls,"," avg(properties.durationMs) as averageLatencyMs,"," quantile(0.95)(properties.durationMs) as p95LatencyMs,"," quantile(0.99)(properties.durationMs) as p99LatencyMs,"," max(properties.durationMs) as maxLatencyMs,"," sum(if(properties.success = 1, 1, 0)) as successCount,"," sum(if(properties.success = 0, 1, 0)) as errorCount","from events",`where ${L(this.eventPrefix,{operations:[e],dateRange:r})}`].join(`
3
+ `),values:E({operations:[e],dateRange:r})}),n=re(s,e,r);if(!n)return null;let o=await this.readTopErrors(e,r);return{...n,topErrors:o}}async readOperationSequences(e){let t=await this.queryHogQL({query:["select"," properties.sequences as sequences","from events","where event = {eventName}",e?.from?"and timestamp >= {dateFrom}":"",e?.to?"and timestamp < {dateTo}":"","order by timestamp desc","limit 50"].filter(Boolean).join(`
4
+ `),values:{eventName:`${this.eventPrefix}.window`,dateFrom:R(e?.from),dateTo:R(e?.to)}});return ne(t)}async readTopErrors(e,t){let r=await this.queryHogQL({query:["select"," properties.errorCode as errorCode,"," count() as errorCount","from events",`where ${L(this.eventPrefix,{operations:[e],dateRange:t})} and properties.success = 0`,"group by errorCode","order by errorCount desc","limit 5"].join(`
5
+ `),values:E({operations:[e],dateRange:t})});return T(r).reduce((n,o)=>{let a=P(o.errorCode);if(!a)return n;return n[a]=u(o.errorCount),n},{})}async queryHogQL(e){if(!this.reader.queryHogQL)throw Error("Analytics reader does not support HogQL queries.");return this.reader.queryHogQL(e)}}function L(e,t){let r=[`event = '${e}.operation'`];if(t.operations?.length)r.push(`(${ee(t.operations)})`);if(t.dateRange?.from)r.push("timestamp >= {dateFrom}");if(t.dateRange?.to)r.push("timestamp < {dateTo}");return r.join(" and ")}function E(e){let t={dateFrom:R(e.dateRange?.from),dateTo:R(e.dateRange?.to)};return e.operations?.forEach((r,s)=>{t[`operationName${s}`]=r.name,t[`operationVersion${s}`]=r.version}),t}function ee(e){return e.map((t,r)=>`(properties.operation = {operationName${r}} and properties.version = {operationVersion${r}})`).join(" or ")}function te(e){return T(e).flatMap((r)=>{let s=P(r.operationName),n=P(r.version),o=ae(r.timestamp);if(!s||!n||!o)return[];return[{operation:{name:s,version:n},durationMs:u(r.durationMs),success:se(r.success),timestamp:o,errorCode:b(r.errorCode)??void 0,tenantId:b(r.tenantId)??void 0,traceId:b(r.traceId)??void 0,actorId:b(r.actorId)??void 0,metadata:B(r.metadata)?r.metadata:void 0}]})}function re(e,t,r){let n=T(e)[0];if(!n)return null;let o=u(n.totalCalls);if(!o)return null;let a=u(n.successCount),l=u(n.errorCount),c=U(r.from)??new Date,i=U(r.to)??new Date;return{operation:t,totalCalls:o,successRate:o?a/o:0,errorRate:o?l/o:0,averageLatencyMs:u(n.averageLatencyMs),p95LatencyMs:u(n.p95LatencyMs),p99LatencyMs:u(n.p99LatencyMs),maxLatencyMs:u(n.maxLatencyMs),windowStart:c,windowEnd:i,topErrors:{}}}function ne(e){let t=T(e),r=new Map;return t.forEach((s)=>{let n=s.sequences;if(!Array.isArray(n))return;n.forEach((o)=>{if(!B(o))return;let a=Array.isArray(o.steps)?o.steps.filter((W)=>typeof W==="string"):[];if(a.length===0)return;let l=typeof o.tenantId==="string"?o.tenantId:void 0,c=typeof o.count==="number"&&Number.isFinite(o.count)?o.count:0,i=`${l??"global"}:${a.join(">")}`,p=r.get(i);if(p)p.count+=c;else r.set(i,{steps:a,tenantId:l,count:c})})}),[...r.values()]}function T(e){if(!Array.isArray(e.results)||!Array.isArray(e.columns))return[];let t=e.columns;return e.results.flatMap((r)=>{if(!Array.isArray(r))return[];let s={};return t.forEach((n,o)=>{s[n]=r[o]}),[s]})}function oe(e){let t=new Date;return{from:new Date(t.getTime()-e*24*60*60*1000),to:t}}function P(e){if(typeof e==="string"&&e.trim())return e;if(typeof e==="number")return String(e);return null}function b(e){if(typeof e==="string")return e;if(typeof e==="number")return String(e);return null}function u(e){if(typeof e==="number"&&Number.isFinite(e))return e;if(typeof e==="string"&&e.trim()){let t=Number(e);if(Number.isFinite(t))return t}return 0}function se(e){if(typeof e==="boolean")return e;if(typeof e==="number")return e!==0;if(typeof e==="string")return e.toLowerCase()==="true";return!1}function ae(e){if(e instanceof Date)return e;if(typeof e==="string"||typeof e==="number"){let t=new Date(e);if(!Number.isNaN(t.getTime()))return t}return null}function R(e){if(!e)return;return typeof e==="string"?e:e.toISOString()}function U(e){if(!e)return null;return e instanceof Date?e:new Date(e)}function B(e){return typeof e==="object"&&e!==null}class Q{provider;eventPrefix;includeMetadata;constructor(e,t={}){this.provider=e,this.eventPrefix=t.eventPrefix??"observability",this.includeMetadata=t.includeMetadata??!1}async captureSample(e){await this.provider.capture({distinctId:e.actorId??e.tenantId??"unknown",event:`${this.eventPrefix}.operation`,timestamp:e.timestamp,properties:{operation:e.operation.name,version:e.operation.version,durationMs:e.durationMs,success:e.success,errorCode:e.errorCode??null,tenantId:e.tenantId??null,traceId:e.traceId??null,...this.includeMetadata&&e.metadata?{metadata:e.metadata}:{}}})}async captureSnapshot(e){await this.provider.capture({distinctId:"system",event:`${this.eventPrefix}.window`,timestamp:e.windowEnd??new Date,properties:{sampleCount:e.sampleCount,metricsCount:e.metrics.length,sequencesCount:e.sequences.length,windowStart:e.windowStart?.toISOString()??null,windowEnd:e.windowEnd?.toISOString()??null,...this.includeMetadata?{metrics:e.metrics.map((t)=>({operation:t.operation.name,version:t.operation.version,totalCalls:t.totalCalls,successRate:t.successRate,errorRate:t.errorRate,averageLatencyMs:t.averageLatencyMs,p95LatencyMs:t.p95LatencyMs,p99LatencyMs:t.p99LatencyMs,maxLatencyMs:t.maxLatencyMs,topErrors:t.topErrors})),sequences:e.sequences}:{}}})}}import{SpanStatusCode as C,trace as ie}from"@opentelemetry/api";var ce="@contractspec/lib.observability";function A(e=ce){return ie.getTracer(e)}async function d(e,t,r){return A(r).startActiveSpan(e,async(n)=>{try{let o=await t(n);return n.setStatus({code:C.OK}),o}catch(o){throw n.recordException(o),n.setStatus({code:C.ERROR,message:o instanceof Error?o.message:String(o)}),o}finally{n.end()}})}function j(e,t,r){return A(r).startActiveSpan(e,(n)=>{try{let o=t(n);return n.setStatus({code:C.OK}),o}catch(o){throw n.recordException(o),n.setStatus({code:C.ERROR,message:o instanceof Error?o.message:String(o)}),o}finally{n.end()}})}async function le(e,t){let r=performance.now();return d("model.selection",async(s)=>{let n=await e(),o=performance.now()-r;if(s.setAttribute("model.selected",t.modelId),s.setAttribute("model.provider",t.providerKey),s.setAttribute("model.score",t.score),s.setAttribute("model.alternatives_count",t.alternativesCount),s.setAttribute("model.selection_duration_ms",o),s.setAttribute("model.reason",t.reason),t.dimension)s.setAttribute("model.dimension",t.dimension);if(t.constraints)s.setAttribute("model.constraints",JSON.stringify(t.constraints));return n})}function pe(e={}){return async(t,r)=>{let s=t.method,o=new URL(t.url).pathname;h.httpRequests.add(1,{method:s,path:o});let a=performance.now();return d(`HTTP ${s} ${o}`,async(l)=>{l.setAttribute("http.method",s),l.setAttribute("http.url",t.url);try{let c=await r();l.setAttribute("http.status_code",c.status);let i=(performance.now()-a)/1000;return h.httpDuration.record(i,{method:s,path:o,status:c.status.toString()}),z({req:t,res:c,span:l,success:!0,durationMs:i*1000,options:e}),c}catch(c){throw h.operationErrors.add(1,{method:s,path:o}),z({req:t,span:l,success:!1,durationMs:performance.now()-a,error:c,options:e}),c}})}}function z({req:e,res:t,span:r,success:s,durationMs:n,error:o,options:a}){if(!a.onSample||!a.resolveOperation)return;let l=a.resolveOperation({req:e,res:t});if(!l)return;let c={operation:l,durationMs:n,success:s,timestamp:new Date,errorCode:!s&&o instanceof Error?o.name:s?void 0:"unknown",tenantId:a.tenantResolver?.(e),actorId:a.actorResolver?.(e),traceId:r.spanContext().traceId,metadata:{method:e.method,path:new URL(e.url).pathname,status:t?.status}};a.onSample(c)}export{j as traceSync,le as traceModelSelection,d as traceAsync,h as standardMetrics,J as logger,A as getTracer,M as getMeter,D as createUpDownCounter,pe as createTracingMiddleware,g as createHistogram,m as createCounter,k as RootCauseAnalyzer,Q as PosthogTelemetryProvider,F as PosthogBaselineReader,H as ModelSelectionTelemetry,I as Logger,N as LifecycleKpiPipeline,S as IntentDetector,f as IntentAggregator,$ as EvolutionPipeline,y as BaselineCalculator,O as AnomalyDetector,q as AlertManager};