@dxos/observability 0.14.0 → 0.16.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/dist/lib/Observability.mjs +1 -248
- package/dist/lib/ai/AiObservability.mjs +1 -238
- package/dist/lib/chunk-AiObservability.mjs +238 -0
- package/dist/lib/chunk-AiObservability.mjs.map +1 -0
- package/dist/lib/chunk-Observability.mjs +248 -0
- package/dist/lib/chunk-Observability.mjs.map +1 -0
- package/dist/lib/chunk-ObservabilityClientProvider.mjs.map +1 -1
- package/dist/lib/chunk-ObservabilityExtension.mjs +38 -11
- package/dist/lib/chunk-ObservabilityExtension.mjs.map +1 -1
- package/dist/lib/chunk-ObservabilityProvider.mjs.map +1 -1
- package/dist/lib/chunk-OtelLogSink.mjs +105 -0
- package/dist/lib/{extensions/otel/OtelLogSink.mjs.map → chunk-OtelLogSink.mjs.map} +1 -1
- package/dist/lib/chunk-OtelMetricsSink.mjs +49 -0
- package/dist/lib/{extensions/otel/OtelMetricsSink.mjs.map → chunk-OtelMetricsSink.mjs.map} +1 -1
- package/dist/lib/chunk-OtelSpanSink.mjs.map +1 -1
- package/dist/lib/chunk-extensions.mjs.map +1 -1
- package/dist/lib/chunk-logs.mjs.map +1 -1
- package/dist/lib/chunk-support-logs.mjs +1 -1
- package/dist/lib/chunk-support-logs.mjs.map +1 -1
- package/dist/lib/extensions/otel/OtelLogSink.mjs +1 -105
- package/dist/lib/extensions/otel/OtelMetricsSink.mjs +1 -49
- package/dist/lib/extensions/otel/OtelSpanSink.mjs +1 -2
- package/dist/lib/index.mjs +4 -4
- package/dist/lib/posthog-transport.node.mjs.map +1 -1
- package/dist/lib/providers/ObservabilityClientProvider.mjs +1 -2
- package/dist/lib/storage.node.mjs.map +1 -1
- package/dist/lib/traces.mjs.map +1 -1
- package/dist/lib/traces.node.mjs.map +1 -1
- package/dist/types/src/ObservabilityExtension.d.ts +1 -1
- package/dist/types/src/ObservabilityExtension.d.ts.map +1 -1
- package/dist/types/src/ai/AiObservability.js +2 -2
- package/dist/types/src/ai/AiObservability.js.map +1 -1
- package/dist/types/src/ai/AiObservability.test.js +11 -11
- package/dist/types/src/ai/AiObservability.test.js.map +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.test.js +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.test.js.map +1 -1
- package/dist/types/src/extensions/posthog/before-send.d.ts +4 -0
- package/dist/types/src/extensions/posthog/before-send.d.ts.map +1 -0
- package/dist/types/src/extensions/posthog/before-send.js +19 -0
- package/dist/types/src/extensions/posthog/before-send.js.map +1 -0
- package/dist/types/src/extensions/posthog/before-send.test.d.ts +2 -0
- package/dist/types/src/extensions/posthog/before-send.test.d.ts.map +1 -0
- package/dist/types/src/extensions/posthog/before-send.test.js +30 -0
- package/dist/types/src/extensions/posthog/before-send.test.js.map +1 -0
- package/dist/types/src/extensions/posthog/extension.d.ts +10 -0
- package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
- package/dist/types/src/extensions/posthog/extension.js +22 -2
- package/dist/types/src/extensions/posthog/extension.js.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.js +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -20
- package/src/ObservabilityExtension.ts +1 -1
- package/src/ai/AiObservability.test.ts +12 -11
- package/src/ai/AiObservability.ts +2 -2
- package/src/extensions/otel/tail-sampling.test.ts +1 -1
- package/src/extensions/posthog/before-send.test.ts +36 -0
- package/src/extensions/posthog/before-send.ts +26 -0
- package/src/extensions/posthog/extension.ts +30 -1
- package/src/version.ts +1 -1
- package/dist/lib/Observability.mjs.map +0 -1
- package/dist/lib/ai/AiObservability.mjs.map +0 -1
|
@@ -1,249 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { t as storage_exports } from "./chunk-storage.mjs";
|
|
3
|
-
import { log } from "@dxos/log";
|
|
4
|
-
import * as Effect from "effect/Effect";
|
|
5
|
-
import { SubscriptionList } from "@dxos/async";
|
|
6
|
-
import * as Array from "effect/Array";
|
|
7
|
-
import * as Function from "effect/Function";
|
|
8
|
-
import { invariant } from "@dxos/invariant";
|
|
1
|
+
export { n as addDataProvider, r as addExtension, i as initialize, a as make } from "./chunk-Observability.mjs";
|
|
9
2
|
export * from "#storage";
|
|
10
|
-
//#region src/Observability.ts
|
|
11
|
-
var Observability_exports = /* @__PURE__ */ __exportAll({
|
|
12
|
-
addDataProvider: () => addDataProvider,
|
|
13
|
-
addExtension: () => addExtension,
|
|
14
|
-
initialize: () => initialize,
|
|
15
|
-
make: () => make
|
|
16
|
-
});
|
|
17
|
-
__reExport(Observability_exports, storage_exports);
|
|
18
|
-
var __dxlog_file = "/__w/dxos/dxos/packages/sdk/observability/src/Observability.ts";
|
|
19
|
-
var attachAiCapture = async (observability) => {
|
|
20
|
-
const { AiSpanProcessor, contentCaptureAllowed } = await import("./ai/AiObservability.mjs");
|
|
21
|
-
const { Otel } = await import("./chunk-extensions.mjs").then((n) => n.t);
|
|
22
|
-
return Otel.addSpanProcessor(new AiSpanProcessor({
|
|
23
|
-
captureInference: (inference) => observability.ai.captureInference(inference),
|
|
24
|
-
captureTurn: (turn) => observability.ai.captureTurn(turn),
|
|
25
|
-
captureToolCall: (toolCall) => observability.ai.captureToolCall(toolCall),
|
|
26
|
-
captureEnabled: () => observability.enabled,
|
|
27
|
-
allowContent: contentCaptureAllowed
|
|
28
|
-
}));
|
|
29
|
-
};
|
|
30
|
-
var ObservabilityImpl = class {
|
|
31
|
-
_initialized = false;
|
|
32
|
-
_extensions = [];
|
|
33
|
-
_dataProviders = [];
|
|
34
|
-
_subscriptions = new SubscriptionList();
|
|
35
|
-
initialize() {
|
|
36
|
-
if (this._initialized) return Effect.succeed(void 0);
|
|
37
|
-
const initializedExtensions = [];
|
|
38
|
-
return Effect.gen({ self: this }, function* () {
|
|
39
|
-
const context = { setTags: (tags, kind) => this.setTags(tags, kind) };
|
|
40
|
-
for (const extension of this._extensions) {
|
|
41
|
-
if (extension.initialize) yield* extension.initialize(context);
|
|
42
|
-
initializedExtensions.push(extension);
|
|
43
|
-
}
|
|
44
|
-
const cleanups = yield* Effect.all(this._dataProviders.map((provider) => provider(this)));
|
|
45
|
-
this._subscriptions.add(...cleanups.filter((cleanup) => cleanup !== void 0));
|
|
46
|
-
this._subscriptions.add(yield* Effect.promise(() => attachAiCapture(this)));
|
|
47
|
-
this._initialized = true;
|
|
48
|
-
}).pipe(Effect.catch((error) => Effect.gen({ self: this }, function* () {
|
|
49
|
-
log.catch(error, void 0, {
|
|
50
|
-
"~LogMeta": "~LogMeta",
|
|
51
|
-
F: __dxlog_file,
|
|
52
|
-
L: 102,
|
|
53
|
-
S: this
|
|
54
|
-
});
|
|
55
|
-
for (const extension of initializedExtensions) if (extension.close) yield* extension.close().pipe(Effect.catch(() => Effect.succeed(void 0)));
|
|
56
|
-
this._subscriptions.clear();
|
|
57
|
-
})));
|
|
58
|
-
}
|
|
59
|
-
close() {
|
|
60
|
-
return Effect.gen({ self: this }, function* () {
|
|
61
|
-
this._subscriptions.clear();
|
|
62
|
-
this._dataProviders.length = 0;
|
|
63
|
-
for (const extension of this._extensions) if (extension.close) yield* extension.close();
|
|
64
|
-
this._initialized = false;
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
enable() {
|
|
68
|
-
return Effect.gen({ self: this }, function* () {
|
|
69
|
-
for (const extension of this._extensions) if (extension.enable) yield* extension.enable();
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
disable() {
|
|
73
|
-
return Effect.gen({ self: this }, function* () {
|
|
74
|
-
for (const extension of this._extensions) if (extension.disable) yield* extension.disable();
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
flush() {
|
|
78
|
-
return Effect.gen({ self: this }, function* () {
|
|
79
|
-
for (const extension of this._extensions) if (extension.flush) yield* extension.flush();
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
_addExtension(extension) {
|
|
83
|
-
invariant(!this._initialized, "Observability is already initialized", {
|
|
84
|
-
"~LogMeta": "~LogMeta",
|
|
85
|
-
F: __dxlog_file,
|
|
86
|
-
L: 159,
|
|
87
|
-
S: this,
|
|
88
|
-
A: ["!this._initialized", "'Observability is already initialized'"]
|
|
89
|
-
});
|
|
90
|
-
this._extensions.push(extension);
|
|
91
|
-
}
|
|
92
|
-
_addDataProvider(dataProvider) {
|
|
93
|
-
invariant(!this._initialized, "Observability is already initialized", {
|
|
94
|
-
"~LogMeta": "~LogMeta",
|
|
95
|
-
F: __dxlog_file,
|
|
96
|
-
L: 164,
|
|
97
|
-
S: this,
|
|
98
|
-
A: ["!this._initialized", "'Observability is already initialized'"]
|
|
99
|
-
});
|
|
100
|
-
this._dataProviders.push(dataProvider);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Adds a data provider and initializes it.
|
|
104
|
-
*/
|
|
105
|
-
addDataProvider(dataProvider) {
|
|
106
|
-
return Effect.gen({ self: this }, function* () {
|
|
107
|
-
this._dataProviders.push(dataProvider);
|
|
108
|
-
const cleanup = yield* dataProvider(this);
|
|
109
|
-
if (cleanup) this._subscriptions.add(cleanup);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
identify(distinctId, attributes, setOnceAttributes) {
|
|
113
|
-
for (const extension of this._extensions) extension.identify?.(distinctId, attributes, setOnceAttributes);
|
|
114
|
-
}
|
|
115
|
-
alias(distinctId, previousId) {
|
|
116
|
-
for (const extension of this._extensions) extension.alias?.(distinctId, previousId);
|
|
117
|
-
}
|
|
118
|
-
setTags(tags, kind) {
|
|
119
|
-
for (const extension of this._extensions) {
|
|
120
|
-
if (kind && !extension.apis.some((api) => api.kind === kind)) continue;
|
|
121
|
-
const processedTags = Object.fromEntries(Object.entries(tags).filter((entry) => entry[1] !== void 0).map(([key, value]) => [key, value.toString()]));
|
|
122
|
-
extension.setTags?.(processedTags, kind);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
get enabled() {
|
|
126
|
-
return this._extensions.every((extension) => extension.enabled);
|
|
127
|
-
}
|
|
128
|
-
get errors() {
|
|
129
|
-
return { captureException: (error, attributes) => {
|
|
130
|
-
for (const extension of this._getExtensions("errors")) extension.captureException(error, attributes);
|
|
131
|
-
} };
|
|
132
|
-
}
|
|
133
|
-
get events() {
|
|
134
|
-
return { captureEvent: (event, attributes) => {
|
|
135
|
-
for (const extension of this._getExtensions("events")) extension.captureEvent(event, attributes);
|
|
136
|
-
} };
|
|
137
|
-
}
|
|
138
|
-
get mcp() {
|
|
139
|
-
return {
|
|
140
|
-
captureInitialize: (client) => {
|
|
141
|
-
for (const extension of this._getExtensions("mcp")) extension.captureInitialize(client);
|
|
142
|
-
},
|
|
143
|
-
captureToolCall: (call) => {
|
|
144
|
-
for (const extension of this._getExtensions("mcp")) extension.captureToolCall(call);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
get ai() {
|
|
149
|
-
return {
|
|
150
|
-
captureInference: (inference) => {
|
|
151
|
-
for (const extension of this._getExtensions("ai")) extension.captureInference(inference);
|
|
152
|
-
},
|
|
153
|
-
captureTurn: (turn) => {
|
|
154
|
-
for (const extension of this._getExtensions("ai")) extension.captureTurn(turn);
|
|
155
|
-
},
|
|
156
|
-
captureToolCall: (toolCall) => {
|
|
157
|
-
for (const extension of this._getExtensions("ai")) extension.captureToolCall(toolCall);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
get support() {
|
|
162
|
-
return {
|
|
163
|
-
uploadLogs: async () => {
|
|
164
|
-
let key;
|
|
165
|
-
for (const extension of this._getExtensions("support")) key = await extension.uploadLogs() ?? key;
|
|
166
|
-
return key;
|
|
167
|
-
},
|
|
168
|
-
uploadNdjson: async (ndjson, kind) => {
|
|
169
|
-
let key;
|
|
170
|
-
for (const extension of this._getExtensions("support")) key = await extension.uploadNdjson(ndjson, kind) ?? key;
|
|
171
|
-
return key;
|
|
172
|
-
},
|
|
173
|
-
sessionContext: () => {
|
|
174
|
-
for (const extension of this._getExtensions("support")) {
|
|
175
|
-
const context = extension.sessionContext();
|
|
176
|
-
if (context) return context;
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
flushLogs: async (attributes) => {
|
|
180
|
-
for (const extension of this._getExtensions("support")) await extension.flushLogs(attributes);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
isAvailable(kind) {
|
|
185
|
-
const apis = this._getExtensions(kind);
|
|
186
|
-
if (apis.length === 0) return Effect.succeed(false);
|
|
187
|
-
return Effect.gen({ self: this }, function* () {
|
|
188
|
-
for (const api of apis) if (yield* api.isAvailable()) return true;
|
|
189
|
-
return false;
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
get metrics() {
|
|
193
|
-
return {
|
|
194
|
-
gauge: (name, value, attributes, meta) => {
|
|
195
|
-
for (const extension of this._getExtensions("metrics")) extension.gauge(name, value, attributes, meta);
|
|
196
|
-
},
|
|
197
|
-
increment: (name, value, attributes, meta) => {
|
|
198
|
-
for (const extension of this._getExtensions("metrics")) extension.increment(name, value, attributes, meta);
|
|
199
|
-
},
|
|
200
|
-
distribution: (name, value, attributes, meta) => {
|
|
201
|
-
for (const extension of this._getExtensions("metrics")) extension.distribution(name, value, attributes, meta);
|
|
202
|
-
},
|
|
203
|
-
observe: (name, callback, attributes, meta) => {
|
|
204
|
-
const cleanups = this._getExtensions("metrics").map((extension) => extension.observe(name, callback, attributes, meta));
|
|
205
|
-
return () => {
|
|
206
|
-
for (const cleanup of cleanups) cleanup();
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
_getExtensions(kind) {
|
|
212
|
-
return Function.pipe(this._extensions, Array.flatMap((extension) => extension.apis), Array.filter((api) => api.kind === kind));
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
var make = () => Effect.succeed(new ObservabilityImpl());
|
|
216
|
-
var addExtension = (_extension) => Effect.fn(function* (_observability) {
|
|
217
|
-
const observability = yield* _observability;
|
|
218
|
-
const extension = yield* _extension;
|
|
219
|
-
invariant("_addExtension" in observability && typeof observability._addExtension === "function", void 0, {
|
|
220
|
-
"~LogMeta": "~LogMeta",
|
|
221
|
-
F: __dxlog_file,
|
|
222
|
-
L: 367,
|
|
223
|
-
S: this,
|
|
224
|
-
A: ["'_addExtension' in observability && typeof observability._addExtension === 'function'", ""]
|
|
225
|
-
});
|
|
226
|
-
observability._addExtension(extension);
|
|
227
|
-
return observability;
|
|
228
|
-
});
|
|
229
|
-
var addDataProvider = (dataProvider) => Effect.fn(function* (_observability) {
|
|
230
|
-
const observability = yield* _observability;
|
|
231
|
-
invariant("_addDataProvider" in observability && typeof observability._addDataProvider === "function", void 0, {
|
|
232
|
-
"~LogMeta": "~LogMeta",
|
|
233
|
-
F: __dxlog_file,
|
|
234
|
-
L: 375,
|
|
235
|
-
S: this,
|
|
236
|
-
A: ["'_addDataProvider' in observability && typeof observability._addDataProvider === 'function'", ""]
|
|
237
|
-
});
|
|
238
|
-
observability._addDataProvider(dataProvider);
|
|
239
|
-
return observability;
|
|
240
|
-
});
|
|
241
|
-
var initialize = Effect.fn(function* (_observability) {
|
|
242
|
-
const observability = yield* _observability;
|
|
243
|
-
yield* observability.initialize();
|
|
244
|
-
return observability;
|
|
245
|
-
});
|
|
246
|
-
//#endregion
|
|
247
|
-
export { addDataProvider, addExtension, initialize, make, Observability_exports as t };
|
|
248
|
-
|
|
249
|
-
//# sourceMappingURL=Observability.mjs.map
|
|
@@ -1,238 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { log } from "@dxos/log";
|
|
3
|
-
import { SpanStatusCode } from "@opentelemetry/api";
|
|
4
|
-
import * as SpanAttributes from "@dxos/effect/SpanAttributes";
|
|
5
|
-
//#region src/ai/AiObservability.ts
|
|
6
|
-
var AiObservability_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
-
AiSpanProcessor: () => AiSpanProcessor,
|
|
8
|
-
contentCaptureAllowed: () => contentCaptureAllowed,
|
|
9
|
-
stripUndefined: () => stripUndefined
|
|
10
|
-
});
|
|
11
|
-
var __dxlog_file = "/__w/dxos/dxos/packages/sdk/observability/src/ai/AiObservability.ts";
|
|
12
|
-
/**
|
|
13
|
-
* AI telemetry capture — data policy.
|
|
14
|
-
*
|
|
15
|
-
* The AI stack already annotates every model call with the OTel GenAI conventions (`gen_ai.*`,
|
|
16
|
-
* emitted by the `effect/ai` provider layers) plus the `dxos.ai.*` attributes those
|
|
17
|
-
* conventions have no room for. This reads the finished spans, applies the capture policy, and
|
|
18
|
-
* hands each surviving call to a sink as a {@link ObservabilityExtension.Inference} — a shape
|
|
19
|
-
* that follows the GenAI conventions, not any vendor's schema. Mapping onto a backend's own
|
|
20
|
-
* vocabulary belongs to that backend's extension.
|
|
21
|
-
*
|
|
22
|
-
* It is the only place the policy is evaluated, so everything leaving passes through it:
|
|
23
|
-
*
|
|
24
|
-
* - **Nothing** unless {@link Options.captureEnabled} reports the user's telemetry opt-in as on.
|
|
25
|
-
* The gate is checked here, before a record is built, rather than left to a backend client's own
|
|
26
|
-
* opt-out flag: that flag is a separate store, so it is not the same answer as the user's
|
|
27
|
-
* DXOS-side preference.
|
|
28
|
-
* - **Metadata** — model, provider, request parameters, token counts (including the prompt-cache
|
|
29
|
-
* reads and writes, which price the call and give the hit rate), latency, trace/span/session ids,
|
|
30
|
-
* error class.
|
|
31
|
-
* - **Content** — prompt, response and tool names, kept only for a span that names its space and
|
|
32
|
-
* whose space {@link Options.allowContent} accepts. Content is always stamped on the span
|
|
33
|
-
* upstream (`AiTelemetry` in `@dxos/ai`) and dropped here, rather than conditionally stamped, so
|
|
34
|
-
* a source that forgets the policy cannot bypass it.
|
|
35
|
-
*
|
|
36
|
-
* Scrub rules enforced here regardless of the above:
|
|
37
|
-
* - A span that does not name its space is treated as unknown, and unknown **denies** content. Only
|
|
38
|
-
* a call that runs in a known space can have its content captured, so the policy fails closed
|
|
39
|
-
* for one that does not (`AiSession` names it, and a process scoped to a space stamps it on every
|
|
40
|
-
* span it runs; a utility model call outside both is reported as metadata only).
|
|
41
|
-
* - Attribute **allowlist**: only the fields of `Inference` are read; every other span attribute
|
|
42
|
-
* is dropped (so an accidental attribute upstream cannot leak through telemetry).
|
|
43
|
-
* - Errors are reduced to the exception **class name** — `exception.message` and
|
|
44
|
-
* `exception.stacktrace` recorded on the span are never forwarded, since provider error
|
|
45
|
-
* messages can embed request or response fragments.
|
|
46
|
-
*
|
|
47
|
-
* Nothing here may throw: `onEnd` runs inside `Span.end()` on the fiber that made the model call,
|
|
48
|
-
* and OTel's `MultiSpanProcessor` does not catch, so an escaping error would fail that call.
|
|
49
|
-
*/
|
|
50
|
-
/**
|
|
51
|
-
* AI telemetry capture policy:
|
|
52
|
-
*
|
|
53
|
-
* | Observability toggle | Space | Capture |
|
|
54
|
-
* |----------------------|---------------------------------------|------------------|
|
|
55
|
-
* | off | any | nothing |
|
|
56
|
-
* | on | EDGE has plaintext access (all today) | metadata+content |
|
|
57
|
-
* | on | E2E-encrypted (future) | metadata only |
|
|
58
|
-
* | on | not declared by the call site | metadata only |
|
|
59
|
-
*
|
|
60
|
-
* Metadata is model, provider, tokens, latency, and trace/session ids. Content adds the prompt,
|
|
61
|
-
* the response, and tool names — including tool results, i.e. data the agent read from the space.
|
|
62
|
-
* The rationale: content that already leaves the device in plaintext for EDGE to replicate is not
|
|
63
|
-
* newly exposed in kind by telemetry, whereas an E2E space promises that plaintext never reaches
|
|
64
|
-
* infrastructure, and telemetry must not become the side channel that breaks it.
|
|
65
|
-
*
|
|
66
|
-
* The last row is the fail-closed default. A space id reaches the span only from a call site that
|
|
67
|
-
* declares one (`AiSession` does; the utility model calls behind summarization, tagging, and
|
|
68
|
-
* extraction do not), so content capture is opt-in per call site and an undeclared one reports
|
|
69
|
-
* metadata only. That is what keeps this predicate honest once it stops returning true: it can
|
|
70
|
-
* never be asked about a space nobody named.
|
|
71
|
-
*
|
|
72
|
-
* Both this predicate and the telemetry opt-in are evaluated in the sink, not at the model call, so
|
|
73
|
-
* they apply to every event on the way out (see `AiSpanProcessor` in `@dxos/observability`, which
|
|
74
|
-
* also holds the scrub rules).
|
|
75
|
-
*/
|
|
76
|
-
var contentCaptureAllowed = (_spaceId) => {
|
|
77
|
-
return true;
|
|
78
|
-
};
|
|
79
|
-
/** The span `effect/ai` names for a streamed call; anything else is a single response. */
|
|
80
|
-
var STREAM_SPAN_NAME = "LanguageModel.streamText";
|
|
81
|
-
/** Marker attributes identifying a GenAI span (per OTel GenAI semantic conventions). */
|
|
82
|
-
var GEN_AI_MARKERS = [
|
|
83
|
-
"gen_ai.system",
|
|
84
|
-
"gen_ai.request.model",
|
|
85
|
-
"gen_ai.response.model"
|
|
86
|
-
];
|
|
87
|
-
/**
|
|
88
|
-
* Session/content attributes stamped by `AiTelemetry.ATTRIBUTES` in `@dxos/ai` (not part of the
|
|
89
|
-
* GenAI spec). Restated rather than imported because telemetry sits below the AI stack, not above
|
|
90
|
-
* it; the `wired to @dxos/ai` cases in `AiObservability.test.ts` drive both halves so a rename fails there.
|
|
91
|
-
*/
|
|
92
|
-
var SESSION_ID_ATTR = SpanAttributes.AI.sessionId;
|
|
93
|
-
/**
|
|
94
|
-
* Shared with the process handle and ECHO rather than AI-specific: a model call inside a
|
|
95
|
-
* space-scoped process carries it whether or not the call site named its space itself.
|
|
96
|
-
*/
|
|
97
|
-
var SPACE_ID_ATTR = SpanAttributes.SPACE_ID;
|
|
98
|
-
var INPUT_ATTR = SpanAttributes.AI.input;
|
|
99
|
-
var OUTPUT_ATTR = SpanAttributes.AI.output;
|
|
100
|
-
var TOOLS_ATTR = SpanAttributes.AI.tools;
|
|
101
|
-
var TRUNCATED_ATTR = SpanAttributes.AI.truncated;
|
|
102
|
-
var CACHE_READ_TOKENS_ATTR = SpanAttributes.AI.cacheReadTokens;
|
|
103
|
-
var CACHE_WRITE_TOKENS_ATTR = SpanAttributes.AI.cacheWriteTokens;
|
|
104
|
-
/** What a non-model span is (`AiTelemetry.KIND`); a span with neither value is not AI at all. */
|
|
105
|
-
var KIND_ATTR = SpanAttributes.AI.kind;
|
|
106
|
-
var KIND_TURN = SpanAttributes.AI_KIND.turn;
|
|
107
|
-
var KIND_TOOL = SpanAttributes.AI_KIND.tool;
|
|
108
|
-
/** Display name for a span whose OTel name is generic, e.g. the tool a `callTool` span ran. */
|
|
109
|
-
var NAME_ATTR = SpanAttributes.AI.name;
|
|
110
|
-
/**
|
|
111
|
-
* Reports finished AI spans to the injected sinks: model calls (`gen_ai.*`) as inferences, and the
|
|
112
|
-
* turn and tool-call spans the AI stack marks with `dxos.ai.kind`. Every other span passes through
|
|
113
|
-
* untouched.
|
|
114
|
-
*
|
|
115
|
-
* Attaches to the realm's tracer provider via `Otel.addSpanProcessor` rather than owning one: the
|
|
116
|
-
* process manager's baseline tracer already records every span, so a second provider would mean a
|
|
117
|
-
* second sampler and a second tracer over the same spans.
|
|
118
|
-
*/
|
|
119
|
-
var AiSpanProcessor = class {
|
|
120
|
-
_captureInference;
|
|
121
|
-
_captureTurn;
|
|
122
|
-
_captureToolCall;
|
|
123
|
-
_captureEnabled;
|
|
124
|
-
_allowContent;
|
|
125
|
-
constructor({ captureInference, captureTurn, captureToolCall, captureEnabled, allowContent }) {
|
|
126
|
-
this._captureInference = captureInference;
|
|
127
|
-
this._captureTurn = captureTurn;
|
|
128
|
-
this._captureToolCall = captureToolCall;
|
|
129
|
-
this._captureEnabled = captureEnabled;
|
|
130
|
-
this._allowContent = allowContent;
|
|
131
|
-
}
|
|
132
|
-
onStart(_span, _parentContext) {}
|
|
133
|
-
onEnd(span) {
|
|
134
|
-
try {
|
|
135
|
-
this._capture(span);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
log.catch(err, { span: span.name }, {
|
|
138
|
-
"~LogMeta": "~LogMeta",
|
|
139
|
-
F: __dxlog_file,
|
|
140
|
-
L: 168,
|
|
141
|
-
S: this
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
_capture(span) {
|
|
146
|
-
const attributes = span.attributes;
|
|
147
|
-
const kind = attributes[KIND_ATTR];
|
|
148
|
-
const isModelCall = GEN_AI_MARKERS.some((key) => attributes[key] !== void 0);
|
|
149
|
-
if (kind !== KIND_TURN && kind !== KIND_TOOL && !isModelCall) return;
|
|
150
|
-
if (!this._captureEnabled()) return;
|
|
151
|
-
const spaceId = attributes[SPACE_ID_ATTR];
|
|
152
|
-
const content = typeof spaceId === "string" && this._allowContent(spaceId);
|
|
153
|
-
if (kind === KIND_TURN || kind === KIND_TOOL) {
|
|
154
|
-
const spanContext = span.spanContext();
|
|
155
|
-
const record = {
|
|
156
|
-
traceId: spanContext.traceId,
|
|
157
|
-
spanId: spanContext.spanId,
|
|
158
|
-
parentSpanId: span.parentSpanContext?.spanId,
|
|
159
|
-
spanName: stringAttribute(attributes[NAME_ATTR]) ?? span.name,
|
|
160
|
-
sessionId: stringAttribute(attributes[SESSION_ID_ATTR]),
|
|
161
|
-
latency: hrTimeToSeconds(span.duration),
|
|
162
|
-
content: content ? {
|
|
163
|
-
input: parseJsonOrRaw(attributes[INPUT_ATTR]),
|
|
164
|
-
output: parseJsonOrRaw(attributes[OUTPUT_ATTR]),
|
|
165
|
-
truncated: attributes[TRUNCATED_ATTR] === true ? true : void 0
|
|
166
|
-
} : void 0,
|
|
167
|
-
errorClass: span.status.code === SpanStatusCode.ERROR ? errorClass(span) : void 0
|
|
168
|
-
};
|
|
169
|
-
if (kind === KIND_TURN) this._captureTurn(record);
|
|
170
|
-
else this._captureToolCall(record);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const spanContext = span.spanContext();
|
|
174
|
-
const inference = {
|
|
175
|
-
traceId: spanContext.traceId,
|
|
176
|
-
spanId: spanContext.spanId,
|
|
177
|
-
parentSpanId: span.parentSpanContext?.spanId,
|
|
178
|
-
spanName: span.name,
|
|
179
|
-
provider: stringAttribute(attributes["gen_ai.system"]),
|
|
180
|
-
model: stringAttribute(attributes["gen_ai.response.model"] ?? attributes["gen_ai.request.model"]),
|
|
181
|
-
sessionId: stringAttribute(attributes[SESSION_ID_ATTR]),
|
|
182
|
-
parameters: modelParameters(attributes),
|
|
183
|
-
inputTokens: numberAttribute(attributes["gen_ai.usage.input_tokens"]),
|
|
184
|
-
outputTokens: numberAttribute(attributes["gen_ai.usage.output_tokens"]),
|
|
185
|
-
cacheReadTokens: numberAttribute(attributes[CACHE_READ_TOKENS_ATTR]),
|
|
186
|
-
cacheWriteTokens: numberAttribute(attributes[CACHE_WRITE_TOKENS_ATTR]),
|
|
187
|
-
latency: hrTimeToSeconds(span.duration),
|
|
188
|
-
streaming: span.name === STREAM_SPAN_NAME,
|
|
189
|
-
content: content ? {
|
|
190
|
-
input: parseJsonOrRaw(attributes[INPUT_ATTR]),
|
|
191
|
-
output: parseJsonOrRaw(attributes[OUTPUT_ATTR]),
|
|
192
|
-
tools: parseJsonOrRaw(attributes[TOOLS_ATTR]),
|
|
193
|
-
truncated: attributes[TRUNCATED_ATTR] === true ? true : void 0
|
|
194
|
-
} : void 0,
|
|
195
|
-
errorClass: span.status.code === SpanStatusCode.ERROR ? errorClass(span) : void 0
|
|
196
|
-
};
|
|
197
|
-
this._captureInference(inference);
|
|
198
|
-
}
|
|
199
|
-
forceFlush() {
|
|
200
|
-
return Promise.resolve();
|
|
201
|
-
}
|
|
202
|
-
shutdown() {
|
|
203
|
-
return Promise.resolve();
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
var hrTimeToSeconds = ([seconds, nanos]) => seconds + nanos / 1e9;
|
|
207
|
-
var stringAttribute = (value) => typeof value === "string" ? value : void 0;
|
|
208
|
-
var numberAttribute = (value) => typeof value === "number" ? value : void 0;
|
|
209
|
-
var modelParameters = (attributes) => {
|
|
210
|
-
const parameters = stripUndefined({
|
|
211
|
-
temperature: attributes["gen_ai.request.temperature"],
|
|
212
|
-
max_tokens: attributes["gen_ai.request.max_tokens"],
|
|
213
|
-
top_p: attributes["gen_ai.request.top_p"],
|
|
214
|
-
top_k: attributes["gen_ai.request.top_k"]
|
|
215
|
-
});
|
|
216
|
-
return Object.keys(parameters).length > 0 ? parameters : void 0;
|
|
217
|
-
};
|
|
218
|
-
var parseJsonOrRaw = (value) => {
|
|
219
|
-
if (typeof value !== "string") return;
|
|
220
|
-
try {
|
|
221
|
-
return JSON.parse(value);
|
|
222
|
-
} catch {
|
|
223
|
-
return value;
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
/** Only the exception class name — messages and stack traces can embed prompt/response content. */
|
|
227
|
-
var errorClass = (span) => {
|
|
228
|
-
for (const event of span.events) {
|
|
229
|
-
const type = event.attributes?.["exception.type"];
|
|
230
|
-
if (typeof type === "string") return type;
|
|
231
|
-
}
|
|
232
|
-
return "Error";
|
|
233
|
-
};
|
|
234
|
-
var stripUndefined = (properties) => Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== void 0));
|
|
235
|
-
//#endregion
|
|
236
|
-
export { AiSpanProcessor, contentCaptureAllowed, stripUndefined, AiObservability_exports as t };
|
|
237
|
-
|
|
238
|
-
//# sourceMappingURL=AiObservability.mjs.map
|
|
1
|
+
export { n as AiSpanProcessor, r as contentCaptureAllowed, i as stripUndefined } from "../chunk-AiObservability.mjs";
|