@illuma-ai/observability-node 0.1.0 → 0.2.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/dist/otel.js ADDED
@@ -0,0 +1,405 @@
1
+ /**
2
+ * OpenTelemetry span processor that converts OTel spans to Illuma Observe traces.
3
+ * Drop-in replacement for @langfuse/otel's LangfuseSpanProcessor.
4
+ *
5
+ * Supports both OpenTelemetry Semantic Conventions for GenAI (`gen_ai.*`)
6
+ * and Vercel AI SDK conventions (`ai.*`).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
11
+ * import { IllumaSpanProcessor } from '@illuma-ai/observability-node/otel';
12
+ *
13
+ * const provider = new NodeTracerProvider();
14
+ * provider.addSpanProcessor(new IllumaSpanProcessor({
15
+ * // Reads from ILLUMA_* / LANGFUSE_* env vars if omitted
16
+ * }));
17
+ * provider.register();
18
+ * ```
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ import { Observability } from './client.js';
23
+ import { ObservationLevel } from '@illuma-ai/observability-core';
24
+ /**
25
+ * OpenTelemetry SpanProcessor that converts OTel spans into Illuma Observe
26
+ * traces and observations (generations / spans).
27
+ *
28
+ * Implements the four-method SpanProcessor contract:
29
+ * - `onStart(span)` — no-op; we process completed spans only
30
+ * - `onEnd(span)` — converts the span to Illuma trace / generation / span
31
+ * - `shutdown()` — flushes and tears down the underlying Observability client
32
+ * - `forceFlush()` — flushes all buffered events immediately
33
+ *
34
+ * Attribute mapping (GenAI Semantic Conventions + Vercel AI SDK):
35
+ * gen_ai.system -> provider
36
+ * gen_ai.request.model / response.model -> model
37
+ * gen_ai.usage.input_tokens / ai.usage.promptTokens -> usage.input
38
+ * gen_ai.usage.output_tokens / ai.usage.completionTokens -> usage.output
39
+ * gen_ai.prompt / ai.prompt.* -> input
40
+ * ai.response.text -> output
41
+ * exception span events -> errorInfo
42
+ */
43
+ export class IllumaSpanProcessor {
44
+ observability;
45
+ /**
46
+ * Maps OTel traceId -> IllumaTraceClient so child spans within the same
47
+ * trace reuse the same Illuma trace.
48
+ */
49
+ traceMap = new Map();
50
+ /**
51
+ * Maps OTel spanId -> Illuma observationId so child spans can reference
52
+ * their parent observation.
53
+ */
54
+ spanIdMap = new Map();
55
+ constructor(options = {}) {
56
+ this.observability = new Observability(options);
57
+ }
58
+ // -------------------------------------------------------------------------
59
+ // SpanProcessor interface
60
+ // -------------------------------------------------------------------------
61
+ /**
62
+ * Called when a span is started.
63
+ * No-op — we only process spans once they are complete (in `onEnd`).
64
+ *
65
+ * @param _span - The started span (unused).
66
+ */
67
+ onStart(_span) {
68
+ // Intentional no-op
69
+ }
70
+ /**
71
+ * Called when a span ends. Converts the OTel span into Illuma Observe
72
+ * trace / generation / span events.
73
+ *
74
+ * @param span - The completed OTel span.
75
+ */
76
+ onEnd(span) {
77
+ const { traceId, spanId } = span.spanContext();
78
+ const parentSpanId = span.parentSpanId;
79
+ const startTime = hrtimeToISO(span.startTime);
80
+ const endTime = hrtimeToISO(span.endTime);
81
+ const input = getSpanInput(span);
82
+ const output = getSpanOutput(span);
83
+ const metadata = getSpanMetadata(span);
84
+ const errorInfo = getErrorInfo(span);
85
+ const isLLMSpan = hasGenAIAttributes(span);
86
+ // 1. Ensure a trace exists for this traceId
87
+ let trace = this.traceMap.get(traceId);
88
+ if (!trace) {
89
+ const traceName = asString(span.attributes['ai.telemetry.metadata.traceName']) ??
90
+ span.name;
91
+ trace = this.observability.trace({
92
+ name: traceName,
93
+ metadata: {
94
+ ...metadata,
95
+ 'otel.traceId': traceId,
96
+ },
97
+ tags: getTraceTags(span),
98
+ ...(span.attributes['ai.telemetry.metadata.threadId'] != null && {
99
+ threadId: String(span.attributes['ai.telemetry.metadata.threadId']),
100
+ }),
101
+ });
102
+ this.traceMap.set(traceId, trace);
103
+ }
104
+ // 2. Resolve parent observation ID (if this span has a parent within our map)
105
+ const parentObservationId = parentSpanId
106
+ ? this.spanIdMap.get(parentSpanId)
107
+ : undefined;
108
+ // 3. Create a generation (LLM call) or a generic span
109
+ if (isLLMSpan) {
110
+ const model = getModel(span);
111
+ const provider = getProvider(span);
112
+ const usage = getUsage(span);
113
+ const gen = trace.generation({
114
+ name: span.name,
115
+ startTime,
116
+ endTime,
117
+ model,
118
+ provider,
119
+ input,
120
+ output,
121
+ usage,
122
+ metadata: {
123
+ ...metadata,
124
+ 'otel.spanId': spanId,
125
+ },
126
+ parentObservationId,
127
+ ...(errorInfo && { level: ObservationLevel.ERROR, statusMessage: errorInfo.message }),
128
+ ...(errorInfo && { errorInfo }),
129
+ });
130
+ this.spanIdMap.set(spanId, gen.id);
131
+ }
132
+ else {
133
+ const s = trace.span({
134
+ name: span.name,
135
+ startTime,
136
+ endTime,
137
+ input,
138
+ output,
139
+ metadata: {
140
+ ...metadata,
141
+ 'otel.spanId': spanId,
142
+ },
143
+ parentObservationId,
144
+ ...(errorInfo && { level: ObservationLevel.ERROR, statusMessage: errorInfo.message }),
145
+ ...(errorInfo && { errorInfo }),
146
+ });
147
+ this.spanIdMap.set(spanId, s.id);
148
+ }
149
+ }
150
+ /**
151
+ * Flush all buffered events and shut down the underlying Observability client.
152
+ * After this call, no further spans should be processed.
153
+ */
154
+ async shutdown() {
155
+ await this.observability.shutdown();
156
+ this.traceMap.clear();
157
+ this.spanIdMap.clear();
158
+ }
159
+ /**
160
+ * Force-flush all buffered events without shutting down.
161
+ */
162
+ async forceFlush() {
163
+ await this.observability.flush();
164
+ }
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Attribute extraction helpers
168
+ // ---------------------------------------------------------------------------
169
+ /** OTel SpanStatusCode.ERROR = 2 */
170
+ const SPAN_STATUS_ERROR = 2;
171
+ /**
172
+ * Convert OTel hrtime [seconds, nanoseconds] to an ISO-8601 timestamp string.
173
+ *
174
+ * @param hrtime - Tuple of [seconds, nanoseconds] from OTel span.
175
+ * @returns ISO-8601 formatted timestamp with millisecond precision.
176
+ */
177
+ function hrtimeToISO(hrtime) {
178
+ const ms = hrtime[0] * 1e3 + hrtime[1] / 1e6;
179
+ return new Date(ms).toISOString();
180
+ }
181
+ /**
182
+ * Check whether a span has gen_ai.* attributes, indicating it wraps an LLM call.
183
+ */
184
+ function hasGenAIAttributes(span) {
185
+ return Object.keys(span.attributes).some((key) => key.startsWith('gen_ai.') || key === 'ai.model.id');
186
+ }
187
+ /**
188
+ * Extract the model identifier from OTel attributes.
189
+ * Checks multiple conventions in priority order.
190
+ */
191
+ function getModel(span) {
192
+ const attrs = span.attributes;
193
+ return (asString(attrs['gen_ai.response.model']) ??
194
+ asString(attrs['gen_ai.request.model']) ??
195
+ asString(attrs['ai.model.id']) ??
196
+ undefined);
197
+ }
198
+ /**
199
+ * Extract the LLM provider from OTel attributes.
200
+ */
201
+ function getProvider(span) {
202
+ const attrs = span.attributes;
203
+ return (asString(attrs['gen_ai.system']) ??
204
+ asString(attrs['ai.model.provider']) ??
205
+ undefined);
206
+ }
207
+ /**
208
+ * Extract token usage from OTel attributes.
209
+ * Supports both GenAI semantic conventions and Vercel AI SDK conventions.
210
+ *
211
+ * @returns Usage object with input/output/total counts, or undefined if no usage data found.
212
+ */
213
+ function getUsage(span) {
214
+ const attrs = span.attributes;
215
+ const inputTokens = asNumber(attrs['gen_ai.usage.input_tokens']) ??
216
+ asNumber(attrs['ai.usage.promptTokens']);
217
+ const outputTokens = asNumber(attrs['gen_ai.usage.output_tokens']) ??
218
+ asNumber(attrs['ai.usage.completionTokens']);
219
+ if (inputTokens == null && outputTokens == null) {
220
+ return undefined;
221
+ }
222
+ return {
223
+ input: inputTokens ?? undefined,
224
+ output: outputTokens ?? undefined,
225
+ total: inputTokens != null || outputTokens != null
226
+ ? (inputTokens ?? 0) + (outputTokens ?? 0)
227
+ : undefined,
228
+ };
229
+ }
230
+ /**
231
+ * Extract the input payload from OTel span attributes.
232
+ * Checks gen_ai.prompt, ai.prompt, gen_ai.request.*, and ai.prompt.* keys.
233
+ *
234
+ * @returns Parsed input object, or undefined if no input data found.
235
+ */
236
+ function getSpanInput(span) {
237
+ const attrs = span.attributes;
238
+ const input = {};
239
+ // Top-level prompt attributes (may be JSON-encoded strings)
240
+ for (const key of ['gen_ai.prompt', 'ai.prompt']) {
241
+ const parsed = tryParseJSON(attrs[key]);
242
+ if (parsed) {
243
+ Object.assign(input, parsed);
244
+ }
245
+ }
246
+ // Namespaced request attributes: gen_ai.request.* -> strip prefix
247
+ for (const [key, value] of Object.entries(attrs)) {
248
+ if (key.startsWith('gen_ai.request.')) {
249
+ input[key.replace('gen_ai.request.', '')] = safeParseJSON(value);
250
+ }
251
+ if (key.startsWith('ai.prompt.')) {
252
+ input[key.replace('ai.prompt.', '')] = safeParseJSON(value);
253
+ }
254
+ }
255
+ if (Object.keys(input).length > 0) {
256
+ return input;
257
+ }
258
+ // Tool call inputs
259
+ if (attrs['ai.toolCall.name'] != null) {
260
+ input.toolName = attrs['ai.toolCall.name'];
261
+ }
262
+ if (attrs['ai.toolCall.args'] != null) {
263
+ input.args = safeParseJSON(attrs['ai.toolCall.args']);
264
+ }
265
+ return Object.keys(input).length > 0 ? input : undefined;
266
+ }
267
+ /**
268
+ * Extract the output payload from OTel span attributes.
269
+ *
270
+ * @returns Parsed output object, or undefined if no output data found.
271
+ */
272
+ function getSpanOutput(span) {
273
+ const attrs = span.attributes;
274
+ if (attrs['ai.response.text'] != null) {
275
+ return { text: attrs['ai.response.text'] };
276
+ }
277
+ if (attrs['ai.response.object'] != null) {
278
+ return { object: safeParseJSON(attrs['ai.response.object']) };
279
+ }
280
+ if (attrs['ai.toolCall.result'] != null) {
281
+ return { result: attrs['ai.toolCall.result'] };
282
+ }
283
+ if (attrs['ai.response.toolCalls'] != null) {
284
+ return { toolCalls: safeParseJSON(attrs['ai.response.toolCalls']) };
285
+ }
286
+ return undefined;
287
+ }
288
+ /**
289
+ * Extract metadata from OTel span attributes (model info, system, etc.).
290
+ */
291
+ function getSpanMetadata(span) {
292
+ const attrs = span.attributes;
293
+ const metadata = {};
294
+ if (attrs['gen_ai.response.model'] != null) {
295
+ metadata.model = attrs['gen_ai.response.model'];
296
+ }
297
+ if (attrs['gen_ai.system'] != null) {
298
+ metadata.system = attrs['gen_ai.system'];
299
+ }
300
+ // Propagate any ai.telemetry.metadata.* keys
301
+ for (const [key, value] of Object.entries(attrs)) {
302
+ if (key.startsWith('ai.telemetry.metadata.') && key !== 'ai.telemetry.metadata.traceName' && key !== 'ai.telemetry.metadata.threadId') {
303
+ metadata[key.replace('ai.telemetry.metadata.', '')] = value;
304
+ }
305
+ }
306
+ return metadata;
307
+ }
308
+ /**
309
+ * Extract structured error information from an OTel span.
310
+ * Looks for `exception` events and falls back to span status message.
311
+ *
312
+ * @returns ErrorInfo if the span has an error status, otherwise undefined.
313
+ */
314
+ function getErrorInfo(span) {
315
+ if (span.status.code !== SPAN_STATUS_ERROR) {
316
+ return undefined;
317
+ }
318
+ const exceptionEvent = span.events.find((event) => event.name === 'exception');
319
+ if (!exceptionEvent) {
320
+ return {
321
+ exceptionType: 'Error',
322
+ message: span.status.message || 'An error occurred',
323
+ traceback: '',
324
+ };
325
+ }
326
+ const eventAttrs = exceptionEvent.attributes ?? {};
327
+ return {
328
+ exceptionType: String(eventAttrs['exception.type'] ?? 'Error'),
329
+ message: eventAttrs['exception.message'] != null
330
+ ? String(eventAttrs['exception.message'])
331
+ : undefined,
332
+ traceback: String(eventAttrs['exception.stacktrace'] ?? ''),
333
+ };
334
+ }
335
+ /**
336
+ * Extract tags from telemetry metadata attributes.
337
+ */
338
+ function getTraceTags(span) {
339
+ const raw = span.attributes['ai.telemetry.metadata.tags'];
340
+ if (raw == null)
341
+ return undefined;
342
+ if (Array.isArray(raw)) {
343
+ return raw.map(String);
344
+ }
345
+ if (typeof raw === 'string') {
346
+ const parsed = safeParseJSON(raw);
347
+ if (Array.isArray(parsed)) {
348
+ return parsed.map(String);
349
+ }
350
+ }
351
+ return undefined;
352
+ }
353
+ // ---------------------------------------------------------------------------
354
+ // JSON / type coercion helpers
355
+ // ---------------------------------------------------------------------------
356
+ /**
357
+ * Safely cast an unknown value to string, returning null if not a string.
358
+ */
359
+ function asString(value) {
360
+ return typeof value === 'string' ? value : null;
361
+ }
362
+ /**
363
+ * Safely cast an unknown value to number, returning null if not numeric.
364
+ */
365
+ function asNumber(value) {
366
+ if (typeof value === 'number')
367
+ return value;
368
+ if (typeof value === 'string') {
369
+ const n = Number(value);
370
+ return Number.isNaN(n) ? null : n;
371
+ }
372
+ return null;
373
+ }
374
+ /**
375
+ * Parse a JSON string, returning the parsed value or the original if parsing fails.
376
+ */
377
+ function safeParseJSON(value) {
378
+ if (typeof value !== 'string')
379
+ return value;
380
+ try {
381
+ return JSON.parse(value);
382
+ }
383
+ catch {
384
+ return value;
385
+ }
386
+ }
387
+ /**
388
+ * Attempt to parse a value as a JSON object. Returns the parsed object
389
+ * only if the result is a non-array, non-null object; otherwise undefined.
390
+ */
391
+ function tryParseJSON(input) {
392
+ if (typeof input !== 'string')
393
+ return undefined;
394
+ try {
395
+ const parsed = JSON.parse(input);
396
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
397
+ return parsed;
398
+ }
399
+ }
400
+ catch {
401
+ // Not valid JSON — ignore
402
+ }
403
+ return undefined;
404
+ }
405
+ //# sourceMappingURL=otel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @track decorator for automatic function tracing.
3
+ *
4
+ * Uses `AsyncLocalStorage` from `node:async_hooks` for context propagation,
5
+ * enabling nested decorators to automatically create child spans under a
6
+ * parent trace. Supports both decorator syntax and function wrapping.
7
+ *
8
+ * Inspired by Opik's track decorator pattern.
9
+ *
10
+ * @example Function wrapping
11
+ * ```ts
12
+ * const tracedFn = track(myFunction, { name: 'my-operation' });
13
+ * await tracedFn(arg1, arg2);
14
+ * ```
15
+ *
16
+ * @example TC39 method decorator
17
+ * ```ts
18
+ * class MyService {
19
+ * @track({ name: 'process' })
20
+ * async process(input: string) { ... }
21
+ * }
22
+ * ```
23
+ *
24
+ * @example Nested tracing (child spans auto-linked to parent trace)
25
+ * ```ts
26
+ * const outer = track(async (x: number) => {
27
+ * return await inner(x + 1); // inner becomes a child span
28
+ * }, { name: 'outer' });
29
+ *
30
+ * const inner = track(async (x: number) => {
31
+ * return x * 2;
32
+ * }, { name: 'inner' });
33
+ * ```
34
+ *
35
+ * @module
36
+ */
37
+ import type { Observability } from './client.js';
38
+ import type { TraceClient, SpanClient } from '@illuma-ai/observability-core';
39
+ /** Internal context propagated via AsyncLocalStorage between nested @track calls. */
40
+ interface TrackContext {
41
+ /** Root trace for this execution tree. */
42
+ trace: TraceClient;
43
+ /** The most recent span in the call stack (undefined at trace root level). */
44
+ currentSpan?: SpanClient;
45
+ /** The Observability client that owns this context. */
46
+ observability: Observability;
47
+ }
48
+ /** Options accepted by the `track` function / decorator. */
49
+ export interface TrackOptions {
50
+ /** Name for the span/trace. Defaults to the wrapped function's name. */
51
+ name?: string;
52
+ /** Custom metadata attached to the span/trace. */
53
+ metadata?: Record<string, unknown>;
54
+ /**
55
+ * Callback invoked with the function's return value before the span is finalized.
56
+ * The returned record is merged into the span/trace update.
57
+ *
58
+ * @param result - The successful return value of the tracked function.
59
+ * @returns Key-value pairs to merge into the span update (e.g. model, usage).
60
+ */
61
+ enrichSpan?: (result: unknown) => Record<string, unknown>;
62
+ /** Observability client instance. If not provided, a singleton is lazily created from env vars. */
63
+ client?: Observability;
64
+ }
65
+ /**
66
+ * Track a function execution as a trace or span.
67
+ *
68
+ * If called within an existing tracked context, creates a child span.
69
+ * Otherwise, creates a new root trace.
70
+ *
71
+ * **Usage patterns:**
72
+ *
73
+ * 1. **Function wrapper** (no options):
74
+ * ```ts
75
+ * const traced = track(myFn);
76
+ * ```
77
+ *
78
+ * 2. **Function wrapper** (with options):
79
+ * ```ts
80
+ * const traced = track(myFn, { name: 'myOp' });
81
+ * ```
82
+ *
83
+ * 3. **Higher-order** (returns a wrapper):
84
+ * ```ts
85
+ * const traced = track({ name: 'myOp' })(myFn);
86
+ * ```
87
+ *
88
+ * 4. **TC39 method decorator**:
89
+ * ```ts
90
+ * class Svc {
91
+ * @track({ name: 'myOp' })
92
+ * async run() { ... }
93
+ * }
94
+ * ```
95
+ *
96
+ * 5. **Legacy (experimental) method decorator**:
97
+ * ```ts
98
+ * class Svc {
99
+ * @track({ name: 'myOp' })
100
+ * async run() { ... }
101
+ * }
102
+ * ```
103
+ *
104
+ * @param fnOrOptions - Either the function to wrap, or a TrackOptions config object.
105
+ * @param optionsOrNothing - When `fnOrOptions` is a function, optional TrackOptions.
106
+ * @returns The wrapped function, or a decorator function.
107
+ */
108
+ export declare function track<T extends (...args: any[]) => any>(fnOrOptions?: T | TrackOptions, optionsOrNothing?: TrackOptions): any;
109
+ /**
110
+ * Retrieve the current tracking context from AsyncLocalStorage.
111
+ *
112
+ * Useful for manual instrumentation when you need access to the active
113
+ * trace or span without using the @track decorator.
114
+ *
115
+ * @returns The current TrackContext, or undefined if not inside a tracked call.
116
+ */
117
+ export declare function getCurrentTrackContext(): TrackContext | undefined;
118
+ /**
119
+ * Reset the cached singleton Observability client.
120
+ *
121
+ * Intended for testing — allows the next `track` call to create a fresh client
122
+ * from environment variables.
123
+ */
124
+ export declare function resetTrackClient(): void;
125
+ export type { TrackContext };
126
+ //# sourceMappingURL=track.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../src/track.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAM7E,qFAAqF;AACrF,UAAU,YAAY;IACpB,0CAA0C;IAC1C,KAAK,EAAE,WAAW,CAAC;IACnB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,uDAAuD;IACvD,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1D,mGAAmG;IACnG,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AA+LD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,wBAAgB,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EACrD,WAAW,CAAC,EAAE,CAAC,GAAG,YAAY,EAC9B,gBAAgB,CAAC,EAAE,YAAY,GAE9B,GAAG,CAkDL;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,YAAY,GAAG,SAAS,CAEjE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}