@contractspec/lib.observability 3.0.0 → 3.1.1

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/AGENTS.md ADDED
@@ -0,0 +1,34 @@
1
+ # AI Agent Guide — `@contractspec/lib.observability`
2
+
3
+ Scope: `packages/libs/observability/*`
4
+
5
+ OpenTelemetry-based observability primitives. Provides tracing, metrics, logging, and anomaly detection for contract-driven systems.
6
+
7
+ ## Quick Context
8
+
9
+ - **Layer**: lib
10
+ - **Consumers**: evolution, progressive-delivery, bundles
11
+
12
+ ## Public Exports
13
+
14
+ - `.` — main entry
15
+ - `./anomaly/*` — anomaly detection sub-modules
16
+ - `./intent/*` — intent tracking sub-modules
17
+ - `./logging` — structured logging integration
18
+ - `./metrics` — metric collection helpers
19
+ - `./pipeline/*` — telemetry pipeline interfaces
20
+ - `./telemetry/*` — telemetry primitives
21
+ - `./tracing` — distributed tracing helpers
22
+
23
+ ## Guardrails
24
+
25
+ - OTel span and metric naming conventions must stay consistent across the platform
26
+ - Pipeline interfaces are adapter boundaries — do not leak vendor-specific details
27
+ - Anomaly detection thresholds affect alerting; changes require validation
28
+
29
+ ## Local Commands
30
+
31
+ - Build: `bun run build`
32
+ - Test: `bun test`
33
+ - Lint: `bun run lint`
34
+ - Dev: `bun run dev`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @contractspec/lib.observability
2
2
 
3
+ ## 3.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [02c0cc5]
8
+ - @contractspec/lib.contracts-integrations@3.1.1
9
+ - @contractspec/lib.contracts-spec@3.1.1
10
+ - @contractspec/lib.lifecycle@3.1.1
11
+
12
+ ## 3.1.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 28987eb: chore: upgrade dependencies
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [f2a4faf]
21
+ - Updated dependencies [28987eb]
22
+ - Updated dependencies [28987eb]
23
+ - @contractspec/lib.contracts-spec@3.1.0
24
+ - @contractspec/lib.contracts-integrations@3.1.0
25
+ - @contractspec/lib.lifecycle@3.1.0
26
+
3
27
  ## 3.0.0
4
28
 
5
29
  ### Major Changes
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { getTracer, traceAsync, traceSync } from './tracing';
2
+ export { traceModelSelection, type ModelSelectionSpanInput, type ModelSelectionSpanAttributes, } from './tracing/model-selection.span';
3
+ export { ModelSelectionTelemetry, type ModelSelectionEventProperties, } from './telemetry/model-selection-telemetry';
2
4
  export { getMeter, createCounter, createUpDownCounter, createHistogram, standardMetrics, } from './metrics';
3
5
  export { Logger, logger } from './logging';
4
6
  export { createTracingMiddleware, type TracingMiddlewareOptions, } from './tracing/middleware';
package/dist/index.js CHANGED
@@ -196,6 +196,56 @@ function traceSync(name, fn, tracerName) {
196
196
  });
197
197
  }
198
198
 
199
+ // src/tracing/model-selection.span.ts
200
+ async function traceModelSelection(fn, input) {
201
+ const startMs = performance.now();
202
+ return traceAsync("model.selection", async (span) => {
203
+ const result = await fn();
204
+ const durationMs = performance.now() - startMs;
205
+ span.setAttribute("model.selected", input.modelId);
206
+ span.setAttribute("model.provider", input.providerKey);
207
+ span.setAttribute("model.score", input.score);
208
+ span.setAttribute("model.alternatives_count", input.alternativesCount);
209
+ span.setAttribute("model.selection_duration_ms", durationMs);
210
+ span.setAttribute("model.reason", input.reason);
211
+ if (input.dimension) {
212
+ span.setAttribute("model.dimension", input.dimension);
213
+ }
214
+ if (input.constraints) {
215
+ span.setAttribute("model.constraints", JSON.stringify(input.constraints));
216
+ }
217
+ return result;
218
+ });
219
+ }
220
+
221
+ // src/telemetry/model-selection-telemetry.ts
222
+ class ModelSelectionTelemetry {
223
+ provider;
224
+ eventName;
225
+ constructor(provider, options) {
226
+ this.provider = provider;
227
+ this.eventName = options?.eventName ?? "$model_selection";
228
+ }
229
+ async trackSelection(distinctId, properties) {
230
+ await this.provider.capture({
231
+ distinctId,
232
+ event: this.eventName,
233
+ timestamp: new Date,
234
+ properties: {
235
+ $model_id: properties.modelId,
236
+ $model_provider: properties.providerKey,
237
+ $model_score: properties.score,
238
+ $model_dimension: properties.dimension ?? null,
239
+ $model_reason: properties.reason,
240
+ $model_alternatives_count: properties.alternativesCount,
241
+ $model_cost_estimate_input: properties.costEstimateInput ?? null,
242
+ $model_cost_estimate_output: properties.costEstimateOutput ?? null,
243
+ $model_selection_duration_ms: properties.selectionDurationMs ?? null
244
+ }
245
+ });
246
+ }
247
+ }
248
+
199
249
  // src/metrics/index.ts
200
250
  import {
201
251
  metrics
@@ -1055,6 +1105,7 @@ function isRecord(value) {
1055
1105
  }
1056
1106
  export {
1057
1107
  traceSync,
1108
+ traceModelSelection,
1058
1109
  traceAsync,
1059
1110
  standardMetrics,
1060
1111
  logger,
@@ -1067,6 +1118,7 @@ export {
1067
1118
  RootCauseAnalyzer,
1068
1119
  PosthogTelemetryProvider,
1069
1120
  PosthogBaselineReader,
1121
+ ModelSelectionTelemetry,
1070
1122
  Logger,
1071
1123
  LifecycleKpiPipeline,
1072
1124
  IntentDetector,
@@ -195,6 +195,56 @@ function traceSync(name, fn, tracerName) {
195
195
  });
196
196
  }
197
197
 
198
+ // src/tracing/model-selection.span.ts
199
+ async function traceModelSelection(fn, input) {
200
+ const startMs = performance.now();
201
+ return traceAsync("model.selection", async (span) => {
202
+ const result = await fn();
203
+ const durationMs = performance.now() - startMs;
204
+ span.setAttribute("model.selected", input.modelId);
205
+ span.setAttribute("model.provider", input.providerKey);
206
+ span.setAttribute("model.score", input.score);
207
+ span.setAttribute("model.alternatives_count", input.alternativesCount);
208
+ span.setAttribute("model.selection_duration_ms", durationMs);
209
+ span.setAttribute("model.reason", input.reason);
210
+ if (input.dimension) {
211
+ span.setAttribute("model.dimension", input.dimension);
212
+ }
213
+ if (input.constraints) {
214
+ span.setAttribute("model.constraints", JSON.stringify(input.constraints));
215
+ }
216
+ return result;
217
+ });
218
+ }
219
+
220
+ // src/telemetry/model-selection-telemetry.ts
221
+ class ModelSelectionTelemetry {
222
+ provider;
223
+ eventName;
224
+ constructor(provider, options) {
225
+ this.provider = provider;
226
+ this.eventName = options?.eventName ?? "$model_selection";
227
+ }
228
+ async trackSelection(distinctId, properties) {
229
+ await this.provider.capture({
230
+ distinctId,
231
+ event: this.eventName,
232
+ timestamp: new Date,
233
+ properties: {
234
+ $model_id: properties.modelId,
235
+ $model_provider: properties.providerKey,
236
+ $model_score: properties.score,
237
+ $model_dimension: properties.dimension ?? null,
238
+ $model_reason: properties.reason,
239
+ $model_alternatives_count: properties.alternativesCount,
240
+ $model_cost_estimate_input: properties.costEstimateInput ?? null,
241
+ $model_cost_estimate_output: properties.costEstimateOutput ?? null,
242
+ $model_selection_duration_ms: properties.selectionDurationMs ?? null
243
+ }
244
+ });
245
+ }
246
+ }
247
+
198
248
  // src/metrics/index.ts
199
249
  import {
200
250
  metrics
@@ -1054,6 +1104,7 @@ function isRecord(value) {
1054
1104
  }
1055
1105
  export {
1056
1106
  traceSync,
1107
+ traceModelSelection,
1057
1108
  traceAsync,
1058
1109
  standardMetrics,
1059
1110
  logger,
@@ -1066,6 +1117,7 @@ export {
1066
1117
  RootCauseAnalyzer,
1067
1118
  PosthogTelemetryProvider,
1068
1119
  PosthogBaselineReader,
1120
+ ModelSelectionTelemetry,
1069
1121
  Logger,
1070
1122
  LifecycleKpiPipeline,
1071
1123
  IntentDetector,
@@ -0,0 +1,30 @@
1
+ // src/telemetry/model-selection-telemetry.ts
2
+ class ModelSelectionTelemetry {
3
+ provider;
4
+ eventName;
5
+ constructor(provider, options) {
6
+ this.provider = provider;
7
+ this.eventName = options?.eventName ?? "$model_selection";
8
+ }
9
+ async trackSelection(distinctId, properties) {
10
+ await this.provider.capture({
11
+ distinctId,
12
+ event: this.eventName,
13
+ timestamp: new Date,
14
+ properties: {
15
+ $model_id: properties.modelId,
16
+ $model_provider: properties.providerKey,
17
+ $model_score: properties.score,
18
+ $model_dimension: properties.dimension ?? null,
19
+ $model_reason: properties.reason,
20
+ $model_alternatives_count: properties.alternativesCount,
21
+ $model_cost_estimate_input: properties.costEstimateInput ?? null,
22
+ $model_cost_estimate_output: properties.costEstimateOutput ?? null,
23
+ $model_selection_duration_ms: properties.selectionDurationMs ?? null
24
+ }
25
+ });
26
+ }
27
+ }
28
+ export {
29
+ ModelSelectionTelemetry
30
+ };
@@ -46,6 +46,28 @@ function traceSync(name, fn, tracerName) {
46
46
  });
47
47
  }
48
48
 
49
+ // src/tracing/model-selection.span.ts
50
+ async function traceModelSelection(fn, input) {
51
+ const startMs = performance.now();
52
+ return traceAsync("model.selection", async (span) => {
53
+ const result = await fn();
54
+ const durationMs = performance.now() - startMs;
55
+ span.setAttribute("model.selected", input.modelId);
56
+ span.setAttribute("model.provider", input.providerKey);
57
+ span.setAttribute("model.score", input.score);
58
+ span.setAttribute("model.alternatives_count", input.alternativesCount);
59
+ span.setAttribute("model.selection_duration_ms", durationMs);
60
+ span.setAttribute("model.reason", input.reason);
61
+ if (input.dimension) {
62
+ span.setAttribute("model.dimension", input.dimension);
63
+ }
64
+ if (input.constraints) {
65
+ span.setAttribute("model.constraints", JSON.stringify(input.constraints));
66
+ }
67
+ return result;
68
+ });
69
+ }
70
+
49
71
  // src/metrics/index.ts
50
72
  import {
51
73
  metrics
@@ -45,8 +45,28 @@ function traceSync(name, fn, tracerName) {
45
45
  }
46
46
  });
47
47
  }
48
+
49
+ // src/tracing/model-selection.span.ts
50
+ async function traceModelSelection(fn, input) {
51
+ const startMs = performance.now();
52
+ return traceAsync("model.selection", async (span) => {
53
+ const result = await fn();
54
+ const durationMs = performance.now() - startMs;
55
+ span.setAttribute("model.selected", input.modelId);
56
+ span.setAttribute("model.provider", input.providerKey);
57
+ span.setAttribute("model.score", input.score);
58
+ span.setAttribute("model.alternatives_count", input.alternativesCount);
59
+ span.setAttribute("model.selection_duration_ms", durationMs);
60
+ span.setAttribute("model.reason", input.reason);
61
+ if (input.dimension) {
62
+ span.setAttribute("model.dimension", input.dimension);
63
+ }
64
+ if (input.constraints) {
65
+ span.setAttribute("model.constraints", JSON.stringify(input.constraints));
66
+ }
67
+ return result;
68
+ });
69
+ }
48
70
  export {
49
- traceSync,
50
- traceAsync,
51
- getTracer
71
+ traceModelSelection
52
72
  };
@@ -0,0 +1,26 @@
1
+ import type { AnalyticsProvider } from '@contractspec/lib.contracts-integrations';
2
+ export interface ModelSelectionEventProperties {
3
+ modelId: string;
4
+ providerKey: string;
5
+ score: number;
6
+ dimension?: string;
7
+ reason: string;
8
+ alternativesCount: number;
9
+ costEstimateInput?: number;
10
+ costEstimateOutput?: number;
11
+ selectionDurationMs?: number;
12
+ }
13
+ /**
14
+ * Track model selection decisions via PostHog analytics.
15
+ *
16
+ * Captures a `$model_selection` event with the selection result properties,
17
+ * enabling analytics dashboards for model usage patterns and cost tracking.
18
+ */
19
+ export declare class ModelSelectionTelemetry {
20
+ private readonly provider;
21
+ private readonly eventName;
22
+ constructor(provider: AnalyticsProvider, options?: {
23
+ eventName?: string;
24
+ });
25
+ trackSelection(distinctId: string, properties: ModelSelectionEventProperties): Promise<void>;
26
+ }
@@ -0,0 +1,31 @@
1
+ // @bun
2
+ // src/telemetry/model-selection-telemetry.ts
3
+ class ModelSelectionTelemetry {
4
+ provider;
5
+ eventName;
6
+ constructor(provider, options) {
7
+ this.provider = provider;
8
+ this.eventName = options?.eventName ?? "$model_selection";
9
+ }
10
+ async trackSelection(distinctId, properties) {
11
+ await this.provider.capture({
12
+ distinctId,
13
+ event: this.eventName,
14
+ timestamp: new Date,
15
+ properties: {
16
+ $model_id: properties.modelId,
17
+ $model_provider: properties.providerKey,
18
+ $model_score: properties.score,
19
+ $model_dimension: properties.dimension ?? null,
20
+ $model_reason: properties.reason,
21
+ $model_alternatives_count: properties.alternativesCount,
22
+ $model_cost_estimate_input: properties.costEstimateInput ?? null,
23
+ $model_cost_estimate_output: properties.costEstimateOutput ?? null,
24
+ $model_selection_duration_ms: properties.selectionDurationMs ?? null
25
+ }
26
+ });
27
+ }
28
+ }
29
+ export {
30
+ ModelSelectionTelemetry
31
+ };
@@ -1,4 +1,5 @@
1
1
  import { type Span, type Tracer } from '@opentelemetry/api';
2
+ export * from './model-selection.span';
2
3
  export declare function getTracer(name?: string): Tracer;
3
4
  export declare function traceAsync<T>(name: string, fn: (span: Span) => Promise<T>, tracerName?: string): Promise<T>;
4
5
  export declare function traceSync<T>(name: string, fn: (span: Span) => T, tracerName?: string): T;
@@ -47,6 +47,28 @@ function traceSync(name, fn, tracerName) {
47
47
  });
48
48
  }
49
49
 
50
+ // src/tracing/model-selection.span.ts
51
+ async function traceModelSelection(fn, input) {
52
+ const startMs = performance.now();
53
+ return traceAsync("model.selection", async (span) => {
54
+ const result = await fn();
55
+ const durationMs = performance.now() - startMs;
56
+ span.setAttribute("model.selected", input.modelId);
57
+ span.setAttribute("model.provider", input.providerKey);
58
+ span.setAttribute("model.score", input.score);
59
+ span.setAttribute("model.alternatives_count", input.alternativesCount);
60
+ span.setAttribute("model.selection_duration_ms", durationMs);
61
+ span.setAttribute("model.reason", input.reason);
62
+ if (input.dimension) {
63
+ span.setAttribute("model.dimension", input.dimension);
64
+ }
65
+ if (input.constraints) {
66
+ span.setAttribute("model.constraints", JSON.stringify(input.constraints));
67
+ }
68
+ return result;
69
+ });
70
+ }
71
+
50
72
  // src/metrics/index.ts
51
73
  import {
52
74
  metrics
@@ -0,0 +1,26 @@
1
+ export interface ModelSelectionSpanAttributes {
2
+ 'model.selected': string;
3
+ 'model.provider': string;
4
+ 'model.score': number;
5
+ 'model.dimension'?: string;
6
+ 'model.alternatives_count': number;
7
+ 'model.constraints'?: string;
8
+ 'model.selection_duration_ms': number;
9
+ 'model.reason': string;
10
+ }
11
+ export interface ModelSelectionSpanInput {
12
+ modelId: string;
13
+ providerKey: string;
14
+ score: number;
15
+ dimension?: string;
16
+ alternativesCount: number;
17
+ constraints?: Record<string, unknown>;
18
+ reason: string;
19
+ }
20
+ /**
21
+ * Trace a model selection decision as an OpenTelemetry span.
22
+ *
23
+ * Wraps an async operation (typically the selector call) and records
24
+ * the selection result as span attributes for distributed tracing.
25
+ */
26
+ export declare function traceModelSelection<T>(fn: () => Promise<T>, input: ModelSelectionSpanInput): Promise<T>;
@@ -46,8 +46,28 @@ function traceSync(name, fn, tracerName) {
46
46
  }
47
47
  });
48
48
  }
49
+
50
+ // src/tracing/model-selection.span.ts
51
+ async function traceModelSelection(fn, input) {
52
+ const startMs = performance.now();
53
+ return traceAsync("model.selection", async (span) => {
54
+ const result = await fn();
55
+ const durationMs = performance.now() - startMs;
56
+ span.setAttribute("model.selected", input.modelId);
57
+ span.setAttribute("model.provider", input.providerKey);
58
+ span.setAttribute("model.score", input.score);
59
+ span.setAttribute("model.alternatives_count", input.alternativesCount);
60
+ span.setAttribute("model.selection_duration_ms", durationMs);
61
+ span.setAttribute("model.reason", input.reason);
62
+ if (input.dimension) {
63
+ span.setAttribute("model.dimension", input.dimension);
64
+ }
65
+ if (input.constraints) {
66
+ span.setAttribute("model.constraints", JSON.stringify(input.constraints));
67
+ }
68
+ return result;
69
+ });
70
+ }
49
71
  export {
50
- traceSync,
51
- traceAsync,
52
- getTracer
72
+ traceModelSelection
53
73
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractspec/lib.observability",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "OpenTelemetry-based observability primitives",
5
5
  "keywords": [
6
6
  "contractspec",
@@ -27,17 +27,17 @@
27
27
  "typecheck": "tsc --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "@contractspec/lib.lifecycle": "3.0.0",
31
- "@contractspec/lib.contracts-spec": "3.0.0",
32
- "@contractspec/lib.contracts-integrations": "3.0.0"
30
+ "@contractspec/lib.lifecycle": "3.1.1",
31
+ "@contractspec/lib.contracts-spec": "3.1.1",
32
+ "@contractspec/lib.contracts-integrations": "3.1.1"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@opentelemetry/api": "*"
36
36
  },
37
37
  "devDependencies": {
38
- "@contractspec/tool.typescript": "3.0.0",
38
+ "@contractspec/tool.typescript": "3.1.0",
39
39
  "typescript": "^5.9.3",
40
- "@contractspec/tool.bun": "3.0.0"
40
+ "@contractspec/tool.bun": "3.1.0"
41
41
  },
42
42
  "exports": {
43
43
  ".": {
@@ -118,6 +118,12 @@
118
118
  "node": "./dist/node/pipeline/lifecycle-pipeline.js",
119
119
  "default": "./dist/pipeline/lifecycle-pipeline.js"
120
120
  },
121
+ "./telemetry/model-selection-telemetry": {
122
+ "types": "./dist/telemetry/model-selection-telemetry.d.ts",
123
+ "bun": "./dist/telemetry/model-selection-telemetry.js",
124
+ "node": "./dist/node/telemetry/model-selection-telemetry.js",
125
+ "default": "./dist/telemetry/model-selection-telemetry.js"
126
+ },
121
127
  "./telemetry/posthog-baseline-reader": {
122
128
  "types": "./dist/telemetry/posthog-baseline-reader.d.ts",
123
129
  "bun": "./dist/telemetry/posthog-baseline-reader.js",
@@ -147,6 +153,12 @@
147
153
  "bun": "./dist/tracing/middleware.js",
148
154
  "node": "./dist/node/tracing/middleware.js",
149
155
  "default": "./dist/tracing/middleware.js"
156
+ },
157
+ "./tracing/model-selection.span": {
158
+ "types": "./dist/tracing/model-selection.span.d.ts",
159
+ "bun": "./dist/tracing/model-selection.span.js",
160
+ "node": "./dist/node/tracing/model-selection.span.js",
161
+ "default": "./dist/tracing/model-selection.span.js"
150
162
  }
151
163
  },
152
164
  "publishConfig": {
@@ -230,6 +242,12 @@
230
242
  "node": "./dist/node/pipeline/lifecycle-pipeline.js",
231
243
  "default": "./dist/pipeline/lifecycle-pipeline.js"
232
244
  },
245
+ "./telemetry/model-selection-telemetry": {
246
+ "types": "./dist/telemetry/model-selection-telemetry.d.ts",
247
+ "bun": "./dist/telemetry/model-selection-telemetry.js",
248
+ "node": "./dist/node/telemetry/model-selection-telemetry.js",
249
+ "default": "./dist/telemetry/model-selection-telemetry.js"
250
+ },
233
251
  "./telemetry/posthog-baseline-reader": {
234
252
  "types": "./dist/telemetry/posthog-baseline-reader.d.ts",
235
253
  "bun": "./dist/telemetry/posthog-baseline-reader.js",
@@ -259,6 +277,12 @@
259
277
  "bun": "./dist/tracing/middleware.js",
260
278
  "node": "./dist/node/tracing/middleware.js",
261
279
  "default": "./dist/tracing/middleware.js"
280
+ },
281
+ "./tracing/model-selection.span": {
282
+ "types": "./dist/tracing/model-selection.span.d.ts",
283
+ "bun": "./dist/tracing/model-selection.span.js",
284
+ "node": "./dist/node/tracing/model-selection.span.js",
285
+ "default": "./dist/tracing/model-selection.span.js"
262
286
  }
263
287
  },
264
288
  "registry": "https://registry.npmjs.org/"