@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.
Files changed (62) hide show
  1. package/dist/lib/Observability.mjs +1 -248
  2. package/dist/lib/ai/AiObservability.mjs +1 -238
  3. package/dist/lib/chunk-AiObservability.mjs +238 -0
  4. package/dist/lib/chunk-AiObservability.mjs.map +1 -0
  5. package/dist/lib/chunk-Observability.mjs +248 -0
  6. package/dist/lib/chunk-Observability.mjs.map +1 -0
  7. package/dist/lib/chunk-ObservabilityClientProvider.mjs.map +1 -1
  8. package/dist/lib/chunk-ObservabilityExtension.mjs +38 -11
  9. package/dist/lib/chunk-ObservabilityExtension.mjs.map +1 -1
  10. package/dist/lib/chunk-ObservabilityProvider.mjs.map +1 -1
  11. package/dist/lib/chunk-OtelLogSink.mjs +105 -0
  12. package/dist/lib/{extensions/otel/OtelLogSink.mjs.map → chunk-OtelLogSink.mjs.map} +1 -1
  13. package/dist/lib/chunk-OtelMetricsSink.mjs +49 -0
  14. package/dist/lib/{extensions/otel/OtelMetricsSink.mjs.map → chunk-OtelMetricsSink.mjs.map} +1 -1
  15. package/dist/lib/chunk-OtelSpanSink.mjs.map +1 -1
  16. package/dist/lib/chunk-extensions.mjs.map +1 -1
  17. package/dist/lib/chunk-logs.mjs.map +1 -1
  18. package/dist/lib/chunk-support-logs.mjs +1 -1
  19. package/dist/lib/chunk-support-logs.mjs.map +1 -1
  20. package/dist/lib/extensions/otel/OtelLogSink.mjs +1 -105
  21. package/dist/lib/extensions/otel/OtelMetricsSink.mjs +1 -49
  22. package/dist/lib/extensions/otel/OtelSpanSink.mjs +1 -2
  23. package/dist/lib/index.mjs +4 -4
  24. package/dist/lib/posthog-transport.node.mjs.map +1 -1
  25. package/dist/lib/providers/ObservabilityClientProvider.mjs +1 -2
  26. package/dist/lib/storage.node.mjs.map +1 -1
  27. package/dist/lib/traces.mjs.map +1 -1
  28. package/dist/lib/traces.node.mjs.map +1 -1
  29. package/dist/types/src/ObservabilityExtension.d.ts +1 -1
  30. package/dist/types/src/ObservabilityExtension.d.ts.map +1 -1
  31. package/dist/types/src/ai/AiObservability.js +2 -2
  32. package/dist/types/src/ai/AiObservability.js.map +1 -1
  33. package/dist/types/src/ai/AiObservability.test.js +11 -11
  34. package/dist/types/src/ai/AiObservability.test.js.map +1 -1
  35. package/dist/types/src/extensions/otel/tail-sampling.test.js +1 -1
  36. package/dist/types/src/extensions/otel/tail-sampling.test.js.map +1 -1
  37. package/dist/types/src/extensions/posthog/before-send.d.ts +4 -0
  38. package/dist/types/src/extensions/posthog/before-send.d.ts.map +1 -0
  39. package/dist/types/src/extensions/posthog/before-send.js +19 -0
  40. package/dist/types/src/extensions/posthog/before-send.js.map +1 -0
  41. package/dist/types/src/extensions/posthog/before-send.test.d.ts +2 -0
  42. package/dist/types/src/extensions/posthog/before-send.test.d.ts.map +1 -0
  43. package/dist/types/src/extensions/posthog/before-send.test.js +30 -0
  44. package/dist/types/src/extensions/posthog/before-send.test.js.map +1 -0
  45. package/dist/types/src/extensions/posthog/extension.d.ts +10 -0
  46. package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
  47. package/dist/types/src/extensions/posthog/extension.js +22 -2
  48. package/dist/types/src/extensions/posthog/extension.js.map +1 -1
  49. package/dist/types/src/version.d.ts +1 -1
  50. package/dist/types/src/version.js +1 -1
  51. package/dist/types/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +20 -20
  53. package/src/ObservabilityExtension.ts +1 -1
  54. package/src/ai/AiObservability.test.ts +12 -11
  55. package/src/ai/AiObservability.ts +2 -2
  56. package/src/extensions/otel/tail-sampling.test.ts +1 -1
  57. package/src/extensions/posthog/before-send.test.ts +36 -0
  58. package/src/extensions/posthog/before-send.ts +26 -0
  59. package/src/extensions/posthog/extension.ts +30 -1
  60. package/src/version.ts +1 -1
  61. package/dist/lib/Observability.mjs.map +0 -1
  62. package/dist/lib/ai/AiObservability.mjs.map +0 -1
@@ -0,0 +1,238 @@
1
+ import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
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.provider.name",
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.provider.name"]),
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 { stripUndefined as i, AiSpanProcessor as n, contentCaptureAllowed as r, AiObservability_exports as t };
237
+
238
+ //# sourceMappingURL=chunk-AiObservability.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-AiObservability.mjs","names":[],"sources":["../../src/ai/AiObservability.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// Consumers import `@dxos/observability/AiObservability`, so only the lazily-activated plugin module\n// that uses the AI sink loads the OpenTelemetry SDK.\n\nimport { type Context, SpanStatusCode } from '@opentelemetry/api';\nimport type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport * as SpanAttributes from '@dxos/effect/SpanAttributes';\nimport { log } from '@dxos/log';\n\nimport type * as ObservabilityExtension from '../ObservabilityExtension.ts';\n\n/**\n * AI telemetry capture — data policy.\n *\n * The AI stack already annotates every model call with the OTel GenAI conventions (`gen_ai.*`,\n * emitted by the `effect/ai` provider layers) plus the `dxos.ai.*` attributes those\n * conventions have no room for. This reads the finished spans, applies the capture policy, and\n * hands each surviving call to a sink as a {@link ObservabilityExtension.Inference} — a shape\n * that follows the GenAI conventions, not any vendor's schema. Mapping onto a backend's own\n * vocabulary belongs to that backend's extension.\n *\n * It is the only place the policy is evaluated, so everything leaving passes through it:\n *\n * - **Nothing** unless {@link Options.captureEnabled} reports the user's telemetry opt-in as on.\n * The gate is checked here, before a record is built, rather than left to a backend client's own\n * opt-out flag: that flag is a separate store, so it is not the same answer as the user's\n * DXOS-side preference.\n * - **Metadata** — model, provider, request parameters, token counts (including the prompt-cache\n * reads and writes, which price the call and give the hit rate), latency, trace/span/session ids,\n * error class.\n * - **Content** — prompt, response and tool names, kept only for a span that names its space and\n * whose space {@link Options.allowContent} accepts. Content is always stamped on the span\n * upstream (`AiTelemetry` in `@dxos/ai`) and dropped here, rather than conditionally stamped, so\n * a source that forgets the policy cannot bypass it.\n *\n * Scrub rules enforced here regardless of the above:\n * - A span that does not name its space is treated as unknown, and unknown **denies** content. Only\n * a call that runs in a known space can have its content captured, so the policy fails closed\n * for one that does not (`AiSession` names it, and a process scoped to a space stamps it on every\n * span it runs; a utility model call outside both is reported as metadata only).\n * - Attribute **allowlist**: only the fields of `Inference` are read; every other span attribute\n * is dropped (so an accidental attribute upstream cannot leak through telemetry).\n * - Errors are reduced to the exception **class name** — `exception.message` and\n * `exception.stacktrace` recorded on the span are never forwarded, since provider error\n * messages can embed request or response fragments.\n *\n * Nothing here may throw: `onEnd` runs inside `Span.end()` on the fiber that made the model call,\n * and OTel's `MultiSpanProcessor` does not catch, so an escaping error would fail that call.\n */\n\n/**\n * AI telemetry capture policy:\n *\n * | Observability toggle | Space | Capture |\n * |----------------------|---------------------------------------|------------------|\n * | off | any | nothing |\n * | on | EDGE has plaintext access (all today) | metadata+content |\n * | on | E2E-encrypted (future) | metadata only |\n * | on | not declared by the call site | metadata only |\n *\n * Metadata is model, provider, tokens, latency, and trace/session ids. Content adds the prompt,\n * the response, and tool names — including tool results, i.e. data the agent read from the space.\n * The rationale: content that already leaves the device in plaintext for EDGE to replicate is not\n * newly exposed in kind by telemetry, whereas an E2E space promises that plaintext never reaches\n * infrastructure, and telemetry must not become the side channel that breaks it.\n *\n * The last row is the fail-closed default. A space id reaches the span only from a call site that\n * declares one (`AiSession` does; the utility model calls behind summarization, tagging, and\n * extraction do not), so content capture is opt-in per call site and an undeclared one reports\n * metadata only. That is what keeps this predicate honest once it stops returning true: it can\n * never be asked about a space nobody named.\n *\n * Both this predicate and the telemetry opt-in are evaluated in the sink, not at the model call, so\n * they apply to every event on the way out (see `AiSpanProcessor` in `@dxos/observability`, which\n * also holds the scrub rules).\n */\nexport const contentCaptureAllowed = (_spaceId: string): boolean => {\n // Always true today because every space replicates through EDGE in plaintext. This MUST NOT\n // stay unconditional: once E2E-encrypted spaces exist this predicate has to return false for\n // them — and apply at the data boundary, not just the conversation's home space, since a turn\n // that reads from an E2E space via a cross-space reference would otherwise leak its content.\n return true;\n};\n\nexport type Options = {\n /** Sink for a call that survived the policy — typically `Observability.ai`. */\n captureInference: (inference: ObservabilityExtension.Inference) => void;\n /** Sink for a conversation turn (`dxos.ai.kind = turn`), the unit its model calls group under. */\n captureTurn: (turn: ObservabilityExtension.Turn) => void;\n /** Sink for a tool call inside a turn (`dxos.ai.kind = tool`). */\n captureToolCall: (toolCall: ObservabilityExtension.ToolCall) => void;\n /** Whether the user's telemetry opt-in is on. Checked before an event is built. */\n captureEnabled: () => boolean;\n /**\n * Whether prompt/response content may leave for the given space. Only called for a span that\n * names one — a span with no space never reaches this predicate and never reports content.\n */\n allowContent: (spaceId: string) => boolean;\n};\n\n/** The span `effect/ai` names for a streamed call; anything else is a single response. */\nconst STREAM_SPAN_NAME = 'LanguageModel.streamText';\n\n/** Marker attributes identifying a GenAI span (per OTel GenAI semantic conventions). */\nconst GEN_AI_MARKERS = ['gen_ai.provider.name', 'gen_ai.request.model', 'gen_ai.response.model'];\n\n/**\n * Session/content attributes stamped by `AiTelemetry.ATTRIBUTES` in `@dxos/ai` (not part of the\n * GenAI spec). Restated rather than imported because telemetry sits below the AI stack, not above\n * it; the `wired to @dxos/ai` cases in `AiObservability.test.ts` drive both halves so a rename fails there.\n */\nconst SESSION_ID_ATTR = SpanAttributes.AI.sessionId;\n/**\n * Shared with the process handle and ECHO rather than AI-specific: a model call inside a\n * space-scoped process carries it whether or not the call site named its space itself.\n */\nconst SPACE_ID_ATTR = SpanAttributes.SPACE_ID;\nconst INPUT_ATTR = SpanAttributes.AI.input;\nconst OUTPUT_ATTR = SpanAttributes.AI.output;\nconst TOOLS_ATTR = SpanAttributes.AI.tools;\nconst TRUNCATED_ATTR = SpanAttributes.AI.truncated;\nconst CACHE_READ_TOKENS_ATTR = SpanAttributes.AI.cacheReadTokens;\nconst CACHE_WRITE_TOKENS_ATTR = SpanAttributes.AI.cacheWriteTokens;\n/** What a non-model span is (`AiTelemetry.KIND`); a span with neither value is not AI at all. */\nconst KIND_ATTR = SpanAttributes.AI.kind;\nconst KIND_TURN = SpanAttributes.AI_KIND.turn;\nconst KIND_TOOL = SpanAttributes.AI_KIND.tool;\n/** Display name for a span whose OTel name is generic, e.g. the tool a `callTool` span ran. */\nconst NAME_ATTR = SpanAttributes.AI.name;\n\n/**\n * Reports finished AI spans to the injected sinks: model calls (`gen_ai.*`) as inferences, and the\n * turn and tool-call spans the AI stack marks with `dxos.ai.kind`. Every other span passes through\n * untouched.\n *\n * Attaches to the realm's tracer provider via `Otel.addSpanProcessor` rather than owning one: the\n * process manager's baseline tracer already records every span, so a second provider would mean a\n * second sampler and a second tracer over the same spans.\n */\nexport class AiSpanProcessor implements SpanProcessor {\n private readonly _captureInference: Options['captureInference'];\n private readonly _captureTurn: Options['captureTurn'];\n private readonly _captureToolCall: Options['captureToolCall'];\n private readonly _captureEnabled: Options['captureEnabled'];\n private readonly _allowContent: Options['allowContent'];\n\n constructor({ captureInference, captureTurn, captureToolCall, captureEnabled, allowContent }: Options) {\n this._captureInference = captureInference;\n this._captureTurn = captureTurn;\n this._captureToolCall = captureToolCall;\n this._captureEnabled = captureEnabled;\n this._allowContent = allowContent;\n }\n\n onStart(_span: Span, _parentContext: Context): void {}\n\n onEnd(span: ReadableSpan): void {\n try {\n this._capture(span);\n } catch (err) {\n // The caller is `Span.end()` on the model call's own fiber; failing telemetry must not fail it.\n log.catch(err, { span: span.name });\n }\n }\n\n private _capture(span: ReadableSpan): void {\n const attributes = span.attributes;\n const kind = attributes[KIND_ATTR];\n const isModelCall = GEN_AI_MARKERS.some((key) => attributes[key] !== undefined);\n if (kind !== KIND_TURN && kind !== KIND_TOOL && !isModelCall) {\n return;\n }\n if (!this._captureEnabled()) {\n return;\n }\n\n const spaceId = attributes[SPACE_ID_ATTR];\n const content = typeof spaceId === 'string' && this._allowContent(spaceId);\n\n if (kind === KIND_TURN || kind === KIND_TOOL) {\n const spanContext = span.spanContext();\n const record: ObservabilityExtension.AiSpanBase = {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId: span.parentSpanContext?.spanId,\n spanName: stringAttribute(attributes[NAME_ATTR]) ?? span.name,\n sessionId: stringAttribute(attributes[SESSION_ID_ATTR]),\n latency: hrTimeToSeconds(span.duration),\n content: content\n ? {\n input: parseJsonOrRaw(attributes[INPUT_ATTR]),\n output: parseJsonOrRaw(attributes[OUTPUT_ATTR]),\n truncated: attributes[TRUNCATED_ATTR] === true ? true : undefined,\n }\n : undefined,\n errorClass: span.status.code === SpanStatusCode.ERROR ? errorClass(span) : undefined,\n };\n if (kind === KIND_TURN) {\n this._captureTurn(record);\n } else {\n this._captureToolCall(record);\n }\n return;\n }\n\n const spanContext = span.spanContext();\n const inference: ObservabilityExtension.Inference = {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n parentSpanId: span.parentSpanContext?.spanId,\n spanName: span.name,\n provider: stringAttribute(attributes['gen_ai.provider.name']),\n model: stringAttribute(attributes['gen_ai.response.model'] ?? attributes['gen_ai.request.model']),\n sessionId: stringAttribute(attributes[SESSION_ID_ATTR]),\n parameters: modelParameters(attributes),\n // The uncached count: add `cacheReadTokens` for what the model actually read.\n inputTokens: numberAttribute(attributes['gen_ai.usage.input_tokens']),\n outputTokens: numberAttribute(attributes['gen_ai.usage.output_tokens']),\n cacheReadTokens: numberAttribute(attributes[CACHE_READ_TOKENS_ATTR]),\n cacheWriteTokens: numberAttribute(attributes[CACHE_WRITE_TOKENS_ATTR]),\n latency: hrTimeToSeconds(span.duration),\n streaming: span.name === STREAM_SPAN_NAME,\n content: content\n ? {\n input: parseJsonOrRaw(attributes[INPUT_ATTR]),\n output: parseJsonOrRaw(attributes[OUTPUT_ATTR]),\n tools: parseJsonOrRaw(attributes[TOOLS_ATTR]),\n // A cut value no longer parses, so it is carried as the raw fragment; saying so keeps a\n // consumer from reading a string where an array was expected as a bug.\n truncated: attributes[TRUNCATED_ATTR] === true ? true : undefined,\n }\n : undefined,\n errorClass: span.status.code === SpanStatusCode.ERROR ? errorClass(span) : undefined,\n };\n\n this._captureInference(inference);\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n}\n\nconst hrTimeToSeconds = ([seconds, nanos]: [number, number]): number => seconds + nanos / 1e9;\n\n// Span attributes are `unknown` to us; narrowing is what keeps a stray value out of the record.\nconst stringAttribute = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);\nconst numberAttribute = (value: unknown): number | undefined => (typeof value === 'number' ? value : undefined);\n\nconst modelParameters = (attributes: ReadableSpan['attributes']): Record<string, unknown> | undefined => {\n const parameters = stripUndefined({\n temperature: attributes['gen_ai.request.temperature'],\n max_tokens: attributes['gen_ai.request.max_tokens'],\n top_p: attributes['gen_ai.request.top_p'],\n top_k: attributes['gen_ai.request.top_k'],\n });\n return Object.keys(parameters).length > 0 ? parameters : undefined;\n};\n\nconst parseJsonOrRaw = (value: unknown): unknown => {\n if (typeof value !== 'string') {\n return undefined;\n }\n try {\n return JSON.parse(value);\n } catch {\n // Truncated content attributes are expected to no longer parse; forward the raw string.\n return value;\n }\n};\n\n/** Only the exception class name — messages and stack traces can embed prompt/response content. */\nconst errorClass = (span: ReadableSpan): string => {\n for (const event of span.events) {\n const type = event.attributes?.['exception.type'];\n if (typeof type === 'string') {\n return type;\n }\n }\n return 'Error';\n};\n\nexport const stripUndefined = (properties: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== undefined));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,IAAa,yBAAyB,aAA8B;CAKlE,OAAO;AACT;;AAmBA,IAAM,mBAAmB;;AAGzB,IAAM,iBAAiB;CAAC;CAAwB;CAAwB;AAAuB;;;;;;AAO/F,IAAM,kBAAkB,eAAe,GAAG;;;;;AAK1C,IAAM,gBAAgB,eAAe;AACrC,IAAM,aAAa,eAAe,GAAG;AACrC,IAAM,cAAc,eAAe,GAAG;AACtC,IAAM,aAAa,eAAe,GAAG;AACrC,IAAM,iBAAiB,eAAe,GAAG;AACzC,IAAM,yBAAyB,eAAe,GAAG;AACjD,IAAM,0BAA0B,eAAe,GAAG;;AAElD,IAAM,YAAY,eAAe,GAAG;AACpC,IAAM,YAAY,eAAe,QAAQ;AACzC,IAAM,YAAY,eAAe,QAAQ;;AAEzC,IAAM,YAAY,eAAe,GAAG;;;;;;;;;;AAWpC,IAAa,kBAAb,MAAsD;CACpD;CACA;CACA;CACA;CACA;CAEA,YAAY,EAAE,kBAAkB,aAAa,iBAAiB,gBAAgB,gBAAyB;EACrG,KAAK,oBAAoB;EACzB,KAAK,eAAe;EACpB,KAAK,mBAAmB;EACxB,KAAK,kBAAkB;EACvB,KAAK,gBAAgB;CACvB;CAEA,QAAQ,OAAa,gBAA+B,CAAC;CAErD,MAAM,MAA0B;EAC9B,IAAI;GACF,KAAK,SAAS,IAAI;EACpB,SAAS,KAAK;GAEZ,IAAI,MAAM,KAAK,EAAE,MAAM,KAAK,KAAK,GAAA;IAAA,YAAA;IAAA,GAAA;IAAA,GAAA;IAAA,GAAA;GAAA,CAAC;EACpC;CACF;CAEA,SAAiB,MAA0B;EACzC,MAAM,aAAa,KAAK;EACxB,MAAM,OAAO,WAAW;EACxB,MAAM,cAAc,eAAe,MAAM,QAAQ,WAAW,SAAS,KAAA,CAAS;EAC9E,IAAI,SAAS,aAAa,SAAS,aAAa,CAAC,aAC/C;EAEF,IAAI,CAAC,KAAK,gBAAgB,GACxB;EAGF,MAAM,UAAU,WAAW;EAC3B,MAAM,UAAU,OAAO,YAAY,YAAY,KAAK,cAAc,OAAO;EAEzE,IAAI,SAAS,aAAa,SAAS,WAAW;GAC5C,MAAM,cAAc,KAAK,YAAY;GACrC,MAAM,SAA4C;IAChD,SAAS,YAAY;IACrB,QAAQ,YAAY;IACpB,cAAc,KAAK,mBAAmB;IACtC,UAAU,gBAAgB,WAAW,UAAU,KAAK,KAAK;IACzD,WAAW,gBAAgB,WAAW,gBAAgB;IACtD,SAAS,gBAAgB,KAAK,QAAQ;IACtC,SAAS,UACL;KACE,OAAO,eAAe,WAAW,WAAW;KAC5C,QAAQ,eAAe,WAAW,YAAY;KAC9C,WAAW,WAAW,oBAAoB,OAAO,OAAO,KAAA;IAC1D,IACA,KAAA;IACJ,YAAY,KAAK,OAAO,SAAS,eAAe,QAAQ,WAAW,IAAI,IAAI,KAAA;GAC7E;GACA,IAAI,SAAS,WACX,KAAK,aAAa,MAAM;QAExB,KAAK,iBAAiB,MAAM;GAE9B;EACF;EAEA,MAAM,cAAc,KAAK,YAAY;EACrC,MAAM,YAA8C;GAClD,SAAS,YAAY;GACrB,QAAQ,YAAY;GACpB,cAAc,KAAK,mBAAmB;GACtC,UAAU,KAAK;GACf,UAAU,gBAAgB,WAAW,uBAAuB;GAC5D,OAAO,gBAAgB,WAAW,4BAA4B,WAAW,uBAAuB;GAChG,WAAW,gBAAgB,WAAW,gBAAgB;GACtD,YAAY,gBAAgB,UAAU;GAEtC,aAAa,gBAAgB,WAAW,4BAA4B;GACpE,cAAc,gBAAgB,WAAW,6BAA6B;GACtE,iBAAiB,gBAAgB,WAAW,uBAAuB;GACnE,kBAAkB,gBAAgB,WAAW,wBAAwB;GACrE,SAAS,gBAAgB,KAAK,QAAQ;GACtC,WAAW,KAAK,SAAS;GACzB,SAAS,UACL;IACE,OAAO,eAAe,WAAW,WAAW;IAC5C,QAAQ,eAAe,WAAW,YAAY;IAC9C,OAAO,eAAe,WAAW,WAAW;IAG5C,WAAW,WAAW,oBAAoB,OAAO,OAAO,KAAA;GAC1D,IACA,KAAA;GACJ,YAAY,KAAK,OAAO,SAAS,eAAe,QAAQ,WAAW,IAAI,IAAI,KAAA;EAC7E;EAEA,KAAK,kBAAkB,SAAS;CAClC;CAEA,aAA4B;EAC1B,OAAO,QAAQ,QAAQ;CACzB;CAEA,WAA0B;EACxB,OAAO,QAAQ,QAAQ;CACzB;AACF;AAEA,IAAM,mBAAmB,CAAC,SAAS,WAAqC,UAAU,QAAQ;AAG1F,IAAM,mBAAmB,UAAwC,OAAO,UAAU,WAAW,QAAQ,KAAA;AACrG,IAAM,mBAAmB,UAAwC,OAAO,UAAU,WAAW,QAAQ,KAAA;AAErG,IAAM,mBAAmB,eAAgF;CACvG,MAAM,aAAa,eAAe;EAChC,aAAa,WAAW;EACxB,YAAY,WAAW;EACvB,OAAO,WAAW;EAClB,OAAO,WAAW;CACpB,CAAC;CACD,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,aAAa,KAAA;AAC3D;AAEA,IAAM,kBAAkB,UAA4B;CAClD,IAAI,OAAO,UAAU,UACnB;CAEF,IAAI;EACF,OAAO,KAAK,MAAM,KAAK;CACzB,QAAQ;EAEN,OAAO;CACT;AACF;;AAGA,IAAM,cAAc,SAA+B;CACjD,KAAK,MAAM,SAAS,KAAK,QAAQ;EAC/B,MAAM,OAAO,MAAM,aAAa;EAChC,IAAI,OAAO,SAAS,UAClB,OAAO;CAEX;CACA,OAAO;AACT;AAEA,IAAa,kBAAkB,eAC7B,OAAO,YAAY,OAAO,QAAQ,UAAU,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CAAC"}
@@ -0,0 +1,248 @@
1
+ import { n as __reExport, t as __exportAll } from "./chunk-rolldown-runtime.mjs";
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";
9
+ //#region src/Observability.ts
10
+ var Observability_exports = /* @__PURE__ */ __exportAll({
11
+ addDataProvider: () => addDataProvider,
12
+ addExtension: () => addExtension,
13
+ initialize: () => initialize,
14
+ make: () => make
15
+ });
16
+ __reExport(Observability_exports, storage_exports);
17
+ var __dxlog_file = "/__w/dxos/dxos/packages/sdk/observability/src/Observability.ts";
18
+ var attachAiCapture = async (observability) => {
19
+ const { AiSpanProcessor, contentCaptureAllowed } = await import("./ai/AiObservability.mjs");
20
+ const { Otel } = await import("./chunk-extensions.mjs").then((n) => n.t);
21
+ return Otel.addSpanProcessor(new AiSpanProcessor({
22
+ captureInference: (inference) => observability.ai.captureInference(inference),
23
+ captureTurn: (turn) => observability.ai.captureTurn(turn),
24
+ captureToolCall: (toolCall) => observability.ai.captureToolCall(toolCall),
25
+ captureEnabled: () => observability.enabled,
26
+ allowContent: contentCaptureAllowed
27
+ }));
28
+ };
29
+ var ObservabilityImpl = class {
30
+ _initialized = false;
31
+ _extensions = [];
32
+ _dataProviders = [];
33
+ _subscriptions = new SubscriptionList();
34
+ initialize() {
35
+ if (this._initialized) return Effect.succeed(void 0);
36
+ const initializedExtensions = [];
37
+ return Effect.gen({ self: this }, function* () {
38
+ const context = { setTags: (tags, kind) => this.setTags(tags, kind) };
39
+ for (const extension of this._extensions) {
40
+ if (extension.initialize) yield* extension.initialize(context);
41
+ initializedExtensions.push(extension);
42
+ }
43
+ const cleanups = yield* Effect.all(this._dataProviders.map((provider) => provider(this)));
44
+ this._subscriptions.add(...cleanups.filter((cleanup) => cleanup !== void 0));
45
+ this._subscriptions.add(yield* Effect.promise(() => attachAiCapture(this)));
46
+ this._initialized = true;
47
+ }).pipe(Effect.catch((error) => Effect.gen({ self: this }, function* () {
48
+ log.catch(error, void 0, {
49
+ "~LogMeta": "~LogMeta",
50
+ F: __dxlog_file,
51
+ L: 102,
52
+ S: this
53
+ });
54
+ for (const extension of initializedExtensions) if (extension.close) yield* extension.close().pipe(Effect.catch(() => Effect.succeed(void 0)));
55
+ this._subscriptions.clear();
56
+ })));
57
+ }
58
+ close() {
59
+ return Effect.gen({ self: this }, function* () {
60
+ this._subscriptions.clear();
61
+ this._dataProviders.length = 0;
62
+ for (const extension of this._extensions) if (extension.close) yield* extension.close();
63
+ this._initialized = false;
64
+ });
65
+ }
66
+ enable() {
67
+ return Effect.gen({ self: this }, function* () {
68
+ for (const extension of this._extensions) if (extension.enable) yield* extension.enable();
69
+ });
70
+ }
71
+ disable() {
72
+ return Effect.gen({ self: this }, function* () {
73
+ for (const extension of this._extensions) if (extension.disable) yield* extension.disable();
74
+ });
75
+ }
76
+ flush() {
77
+ return Effect.gen({ self: this }, function* () {
78
+ for (const extension of this._extensions) if (extension.flush) yield* extension.flush();
79
+ });
80
+ }
81
+ _addExtension(extension) {
82
+ invariant(!this._initialized, "Observability is already initialized", {
83
+ "~LogMeta": "~LogMeta",
84
+ F: __dxlog_file,
85
+ L: 159,
86
+ S: this,
87
+ A: ["!this._initialized", "'Observability is already initialized'"]
88
+ });
89
+ this._extensions.push(extension);
90
+ }
91
+ _addDataProvider(dataProvider) {
92
+ invariant(!this._initialized, "Observability is already initialized", {
93
+ "~LogMeta": "~LogMeta",
94
+ F: __dxlog_file,
95
+ L: 164,
96
+ S: this,
97
+ A: ["!this._initialized", "'Observability is already initialized'"]
98
+ });
99
+ this._dataProviders.push(dataProvider);
100
+ }
101
+ /**
102
+ * Adds a data provider and initializes it.
103
+ */
104
+ addDataProvider(dataProvider) {
105
+ return Effect.gen({ self: this }, function* () {
106
+ this._dataProviders.push(dataProvider);
107
+ const cleanup = yield* dataProvider(this);
108
+ if (cleanup) this._subscriptions.add(cleanup);
109
+ });
110
+ }
111
+ identify(distinctId, attributes, setOnceAttributes) {
112
+ for (const extension of this._extensions) extension.identify?.(distinctId, attributes, setOnceAttributes);
113
+ }
114
+ alias(distinctId, previousId) {
115
+ for (const extension of this._extensions) extension.alias?.(distinctId, previousId);
116
+ }
117
+ setTags(tags, kind) {
118
+ for (const extension of this._extensions) {
119
+ if (kind && !extension.apis.some((api) => api.kind === kind)) continue;
120
+ const processedTags = Object.fromEntries(Object.entries(tags).filter((entry) => entry[1] !== void 0).map(([key, value]) => [key, value.toString()]));
121
+ extension.setTags?.(processedTags, kind);
122
+ }
123
+ }
124
+ get enabled() {
125
+ return this._extensions.every((extension) => extension.enabled);
126
+ }
127
+ get errors() {
128
+ return { captureException: (error, attributes) => {
129
+ for (const extension of this._getExtensions("errors")) extension.captureException(error, attributes);
130
+ } };
131
+ }
132
+ get events() {
133
+ return { captureEvent: (event, attributes) => {
134
+ for (const extension of this._getExtensions("events")) extension.captureEvent(event, attributes);
135
+ } };
136
+ }
137
+ get mcp() {
138
+ return {
139
+ captureInitialize: (client) => {
140
+ for (const extension of this._getExtensions("mcp")) extension.captureInitialize(client);
141
+ },
142
+ captureToolCall: (call) => {
143
+ for (const extension of this._getExtensions("mcp")) extension.captureToolCall(call);
144
+ }
145
+ };
146
+ }
147
+ get ai() {
148
+ return {
149
+ captureInference: (inference) => {
150
+ for (const extension of this._getExtensions("ai")) extension.captureInference(inference);
151
+ },
152
+ captureTurn: (turn) => {
153
+ for (const extension of this._getExtensions("ai")) extension.captureTurn(turn);
154
+ },
155
+ captureToolCall: (toolCall) => {
156
+ for (const extension of this._getExtensions("ai")) extension.captureToolCall(toolCall);
157
+ }
158
+ };
159
+ }
160
+ get support() {
161
+ return {
162
+ uploadLogs: async () => {
163
+ let key;
164
+ for (const extension of this._getExtensions("support")) key = await extension.uploadLogs() ?? key;
165
+ return key;
166
+ },
167
+ uploadNdjson: async (ndjson, kind) => {
168
+ let key;
169
+ for (const extension of this._getExtensions("support")) key = await extension.uploadNdjson(ndjson, kind) ?? key;
170
+ return key;
171
+ },
172
+ sessionContext: () => {
173
+ for (const extension of this._getExtensions("support")) {
174
+ const context = extension.sessionContext();
175
+ if (context) return context;
176
+ }
177
+ },
178
+ flushLogs: async (attributes) => {
179
+ for (const extension of this._getExtensions("support")) await extension.flushLogs(attributes);
180
+ }
181
+ };
182
+ }
183
+ isAvailable(kind) {
184
+ const apis = this._getExtensions(kind);
185
+ if (apis.length === 0) return Effect.succeed(false);
186
+ return Effect.gen({ self: this }, function* () {
187
+ for (const api of apis) if (yield* api.isAvailable()) return true;
188
+ return false;
189
+ });
190
+ }
191
+ get metrics() {
192
+ return {
193
+ gauge: (name, value, attributes, meta) => {
194
+ for (const extension of this._getExtensions("metrics")) extension.gauge(name, value, attributes, meta);
195
+ },
196
+ increment: (name, value, attributes, meta) => {
197
+ for (const extension of this._getExtensions("metrics")) extension.increment(name, value, attributes, meta);
198
+ },
199
+ distribution: (name, value, attributes, meta) => {
200
+ for (const extension of this._getExtensions("metrics")) extension.distribution(name, value, attributes, meta);
201
+ },
202
+ observe: (name, callback, attributes, meta) => {
203
+ const cleanups = this._getExtensions("metrics").map((extension) => extension.observe(name, callback, attributes, meta));
204
+ return () => {
205
+ for (const cleanup of cleanups) cleanup();
206
+ };
207
+ }
208
+ };
209
+ }
210
+ _getExtensions(kind) {
211
+ return Function.pipe(this._extensions, Array.flatMap((extension) => extension.apis), Array.filter((api) => api.kind === kind));
212
+ }
213
+ };
214
+ var make = () => Effect.succeed(new ObservabilityImpl());
215
+ var addExtension = (_extension) => Effect.fn(function* (_observability) {
216
+ const observability = yield* _observability;
217
+ const extension = yield* _extension;
218
+ invariant("_addExtension" in observability && typeof observability._addExtension === "function", void 0, {
219
+ "~LogMeta": "~LogMeta",
220
+ F: __dxlog_file,
221
+ L: 367,
222
+ S: this,
223
+ A: ["'_addExtension' in observability && typeof observability._addExtension === 'function'", ""]
224
+ });
225
+ observability._addExtension(extension);
226
+ return observability;
227
+ });
228
+ var addDataProvider = (dataProvider) => Effect.fn(function* (_observability) {
229
+ const observability = yield* _observability;
230
+ invariant("_addDataProvider" in observability && typeof observability._addDataProvider === "function", void 0, {
231
+ "~LogMeta": "~LogMeta",
232
+ F: __dxlog_file,
233
+ L: 375,
234
+ S: this,
235
+ A: ["'_addDataProvider' in observability && typeof observability._addDataProvider === 'function'", ""]
236
+ });
237
+ observability._addDataProvider(dataProvider);
238
+ return observability;
239
+ });
240
+ var initialize = Effect.fn(function* (_observability) {
241
+ const observability = yield* _observability;
242
+ yield* observability.initialize();
243
+ return observability;
244
+ });
245
+ //#endregion
246
+ export { make as a, initialize as i, addDataProvider as n, addExtension as r, Observability_exports as t };
247
+
248
+ //# sourceMappingURL=chunk-Observability.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-Observability.mjs","names":[],"sources":["../../src/Observability.ts"],"sourcesContent":["//\n// Copyright 2025 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Array from 'effect/Array';\nimport * as Effect from 'effect/Effect';\nimport * as Function from 'effect/Function';\n\nimport { type CleanupFn, SubscriptionList } from '@dxos/async';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\n\nimport * as ObservabilityExtension from './ObservabilityExtension.ts';\n\nexport * from './storage/index.ts';\n\nconst attachAiCapture = async (observability: Observability): Promise<CleanupFn> => {\n const { AiSpanProcessor, contentCaptureAllowed } = await import('./ai/AiObservability.ts');\n const { Otel } = await import('./extensions/index.ts');\n return Otel.addSpanProcessor(\n new AiSpanProcessor({\n captureInference: (inference) => observability.ai.captureInference(inference),\n captureTurn: (turn) => observability.ai.captureTurn(turn),\n captureToolCall: (toolCall) => observability.ai.captureToolCall(toolCall),\n captureEnabled: () => observability.enabled,\n allowContent: contentCaptureAllowed,\n }),\n );\n};\n\n// TODO(wittjosiah): Figure out how to handle when telemetry is disabled.\n// In theory the setting should be both persisted and synchronized.\n// Initialize probably should still run for the cases where data is emitted manually (e.g., support tickets).\n\n/**\n * Provider of observability data.\n */\nexport type DataProvider = (observability: Observability) => Effect.Effect<CleanupFn | void, Error>;\n\n/**\n * Extensible observability provider.\n */\n// TODO(wittjosiah): Add pipe method.\nexport interface Observability {\n initialize(): Effect.Effect<void>;\n close(): Effect.Effect<void>;\n enable(): Effect.Effect<void>;\n disable(): Effect.Effect<void>;\n flush(): Effect.Effect<void>;\n addDataProvider(dataProvider: DataProvider): Effect.Effect<void, Error>;\n identify(\n distinctId: string,\n attributes?: ObservabilityExtension.Attributes,\n setOnceAttributes?: ObservabilityExtension.Attributes,\n ): void;\n alias(distinctId: string, previousId?: string): void;\n setTags(tags: ObservabilityExtension.Attributes, kind?: ObservabilityExtension.Kind): void;\n enabled: boolean;\n errors: ObservabilityExtension.Errors;\n events: ObservabilityExtension.Events;\n support: ObservabilityExtension.Support;\n ai: ObservabilityExtension.Ai;\n mcp: ObservabilityExtension.Mcp;\n /** True if at least one extension of the given kind reports as available. */\n isAvailable(kind: ObservabilityExtension.Kind): Effect.Effect<boolean>;\n metrics: ObservabilityExtension.Metrics;\n}\n\nclass ObservabilityImpl implements Observability {\n private _initialized = false;\n private readonly _extensions: ObservabilityExtension.Extension[] = [];\n private readonly _dataProviders: DataProvider[] = [];\n private readonly _subscriptions = new SubscriptionList();\n\n initialize(): Effect.Effect<void> {\n if (this._initialized) {\n return Effect.succeed(undefined);\n }\n\n const initializedExtensions: ObservabilityExtension.Extension[] = [];\n\n return Effect.gen({ self: this }, function* () {\n const context: ObservabilityExtension.ExtensionContext = {\n setTags: (tags, kind) => this.setTags(tags, kind),\n };\n for (const extension of this._extensions) {\n if (extension.initialize) {\n yield* extension.initialize(context);\n }\n initializedExtensions.push(extension);\n }\n\n const cleanups = yield* Effect.all(this._dataProviders.map((provider) => provider(this)));\n this._subscriptions.add(...cleanups.filter((cleanup) => cleanup !== undefined));\n this._subscriptions.add(yield* Effect.promise(() => attachAiCapture(this)));\n this._initialized = true;\n }).pipe(\n Effect.catch((error) =>\n Effect.gen({ self: this }, function* () {\n log.catch(error);\n // Roll back already-initialized extensions.\n for (const extension of initializedExtensions) {\n if (extension.close) {\n yield* extension.close().pipe(Effect.catch(() => Effect.succeed(undefined)));\n }\n }\n this._subscriptions.clear();\n }),\n ),\n );\n }\n\n close(): Effect.Effect<void> {\n return Effect.gen({ self: this }, function* () {\n this._subscriptions.clear();\n this._dataProviders.length = 0;\n for (const extension of this._extensions) {\n if (extension.close) {\n yield* extension.close();\n }\n }\n this._initialized = false;\n });\n }\n\n enable(): Effect.Effect<void> {\n return Effect.gen({ self: this }, function* () {\n for (const extension of this._extensions) {\n if (extension.enable) {\n yield* extension.enable();\n }\n }\n });\n }\n\n disable(): Effect.Effect<void> {\n return Effect.gen({ self: this }, function* () {\n for (const extension of this._extensions) {\n if (extension.disable) {\n yield* extension.disable();\n }\n }\n });\n }\n\n flush(): Effect.Effect<void> {\n return Effect.gen({ self: this }, function* () {\n for (const extension of this._extensions) {\n if (extension.flush) {\n yield* extension.flush();\n }\n }\n });\n }\n\n _addExtension(extension: ObservabilityExtension.Extension): void {\n invariant(!this._initialized, 'Observability is already initialized');\n this._extensions.push(extension);\n }\n\n _addDataProvider(dataProvider: DataProvider): void {\n invariant(!this._initialized, 'Observability is already initialized');\n this._dataProviders.push(dataProvider);\n }\n\n /**\n * Adds a data provider and initializes it.\n */\n addDataProvider(dataProvider: DataProvider): Effect.Effect<void, Error> {\n return Effect.gen({ self: this }, function* () {\n this._dataProviders.push(dataProvider);\n const cleanup = yield* dataProvider(this);\n if (cleanup) {\n this._subscriptions.add(cleanup);\n }\n });\n }\n\n identify(\n distinctId: string,\n attributes?: ObservabilityExtension.Attributes,\n setOnceAttributes?: ObservabilityExtension.Attributes,\n ): void {\n for (const extension of this._extensions) {\n extension.identify?.(distinctId, attributes, setOnceAttributes);\n }\n }\n\n alias(distinctId: string, previousId?: string): void {\n for (const extension of this._extensions) {\n extension.alias?.(distinctId, previousId);\n }\n }\n\n setTags(tags: ObservabilityExtension.Attributes, kind?: ObservabilityExtension.Kind): void {\n for (const extension of this._extensions) {\n if (kind && !extension.apis.some((api) => api.kind === kind)) {\n continue;\n }\n\n const processedTags = Object.fromEntries(\n Object.entries(tags)\n .filter((entry): entry is [string, string | number | boolean] => entry[1] !== undefined)\n .map(([key, value]) => [key, value.toString()]),\n );\n extension.setTags?.(processedTags, kind);\n }\n }\n\n get enabled(): boolean {\n return this._extensions.every((extension) => extension.enabled);\n }\n\n get errors(): ObservabilityExtension.Errors {\n return {\n captureException: (error, attributes) => {\n for (const extension of this._getExtensions('errors')) {\n extension.captureException(error, attributes);\n }\n },\n };\n }\n\n get events(): ObservabilityExtension.Events {\n return {\n captureEvent: (event, attributes) => {\n for (const extension of this._getExtensions('events')) {\n extension.captureEvent(event, attributes);\n }\n },\n };\n }\n\n get mcp(): ObservabilityExtension.Mcp {\n return {\n captureInitialize: (client) => {\n for (const extension of this._getExtensions('mcp')) {\n extension.captureInitialize(client);\n }\n },\n captureToolCall: (call) => {\n for (const extension of this._getExtensions('mcp')) {\n extension.captureToolCall(call);\n }\n },\n };\n }\n\n get ai(): ObservabilityExtension.Ai {\n return {\n captureInference: (inference) => {\n for (const extension of this._getExtensions('ai')) {\n extension.captureInference(inference);\n }\n },\n captureTurn: (turn) => {\n for (const extension of this._getExtensions('ai')) {\n extension.captureTurn(turn);\n }\n },\n captureToolCall: (toolCall) => {\n for (const extension of this._getExtensions('ai')) {\n extension.captureToolCall(toolCall);\n }\n },\n };\n }\n\n get support(): ObservabilityExtension.Support {\n return {\n uploadLogs: async () => {\n let key: string | undefined;\n for (const extension of this._getExtensions('support')) {\n key = (await extension.uploadLogs()) ?? key;\n }\n return key;\n },\n uploadNdjson: async (ndjson, kind) => {\n let key: string | undefined;\n for (const extension of this._getExtensions('support')) {\n key = (await extension.uploadNdjson(ndjson, kind)) ?? key;\n }\n return key;\n },\n sessionContext: () => {\n for (const extension of this._getExtensions('support')) {\n const context = extension.sessionContext();\n if (context) {\n return context;\n }\n }\n return undefined;\n },\n flushLogs: async (attributes) => {\n for (const extension of this._getExtensions('support')) {\n await extension.flushLogs(attributes);\n }\n },\n };\n }\n\n isAvailable(kind: ObservabilityExtension.Kind): Effect.Effect<boolean> {\n const apis = this._getExtensions(kind);\n if (apis.length === 0) {\n return Effect.succeed(false);\n }\n return Effect.gen({ self: this }, function* () {\n for (const api of apis) {\n const available = yield* api.isAvailable();\n if (available) {\n return true;\n }\n }\n return false;\n });\n }\n\n get metrics(): ObservabilityExtension.Metrics {\n return {\n gauge: (name, value, attributes, meta) => {\n for (const extension of this._getExtensions('metrics')) {\n extension.gauge(name, value, attributes, meta);\n }\n },\n increment: (name, value, attributes, meta) => {\n for (const extension of this._getExtensions('metrics')) {\n extension.increment(name, value, attributes, meta);\n }\n },\n distribution: (name, value, attributes, meta) => {\n for (const extension of this._getExtensions('metrics')) {\n extension.distribution(name, value, attributes, meta);\n }\n },\n observe: (name, callback, attributes, meta) => {\n const cleanups = this._getExtensions('metrics').map((extension) =>\n extension.observe(name, callback, attributes, meta),\n );\n return () => {\n for (const cleanup of cleanups) {\n cleanup();\n }\n };\n },\n };\n }\n\n private _getExtensions<T extends ObservabilityExtension.Kind>(\n kind: T,\n ): Extract<ObservabilityExtension.ExtensionApi, { kind: T }>[] {\n return Function.pipe(\n this._extensions,\n Array.flatMap((extension) => extension.apis),\n Array.filter((api): api is Extract<ObservabilityExtension.ExtensionApi, { kind: T }> => api.kind === kind),\n );\n }\n}\n\nexport const make = (): Effect.Effect<Observability> => Effect.succeed(new ObservabilityImpl());\n\nexport const addExtension = (_extension: Effect.Effect<ObservabilityExtension.Extension>) =>\n Effect.fn(function* (_observability: Effect.Effect<Observability>) {\n const observability = yield* _observability;\n const extension = yield* _extension;\n invariant('_addExtension' in observability && typeof observability._addExtension === 'function');\n observability._addExtension(extension);\n return observability;\n });\n\nexport const addDataProvider = (dataProvider: DataProvider) =>\n Effect.fn(function* (_observability: Effect.Effect<Observability>) {\n const observability = yield* _observability;\n invariant('_addDataProvider' in observability && typeof observability._addDataProvider === 'function');\n observability._addDataProvider(dataProvider);\n return observability;\n });\n\nexport const initialize = Effect.fn(function* (_observability: Effect.Effect<Observability>) {\n const observability = yield* _observability;\n yield* observability.initialize();\n return observability;\n});\n"],"mappings":"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"}