import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { OpenInferenceSpanKind, SemanticConventions, } from "@arizeai/openinference-semantic-conventions"; import type { OITracer } from "../trace"; import { isPromise } from "../utils/typeUtils"; import { defaultProcessInput, defaultProcessOutput } from "./attributeHelpers"; import { getTracer, wrapTracer } from "./tracerHelpers"; import type { AnyFn, InputToAttributesFn, OutputToAttributesFn, SpanTraceOptions } from "./types"; const { OPENINFERENCE_SPAN_KIND } = SemanticConventions; /** * Wraps a function with openinference tracing capabilities, creating spans for execution monitoring. * * This function provides comprehensive tracing for both synchronous and asynchronous functions, * automatically handling span lifecycle, input/output processing, error tracking, and promise * resolution. * * Agent-facing behavior to rely on: * - Preserves the call-time `this` value, so wrapped methods still work when invoked as methods * or via `.call()` / `.apply()` * - Records both synchronous throws and rejected promises on the span, marks the span as ERROR, * ends the span, and re-throws the original error * - Resolves the default tracer when the wrapped function is invoked, so wrappers created before * a global tracer provider change pick up the latest provider unless `options.tracer` was set * * @experimental This API is experimental and may change in future versions * * @template Fn - The function type being wrapped, preserving original signature * @param fn - The function to wrap with tracing capabilities * @param options - Configuration options for tracing behavior * @param options.tracer - Custom OpenTelemetry tracer instance (otherwise the current global tracer * provider is resolved when the wrapper is invoked) * @param options.name - Custom span name (defaults to function name) * @param options.openTelemetrySpanKind - OpenTelemetry span kind (defaults to INTERNAL) * @param options.kind - OpenInference span kind for semantic categorization (defaults to CHAIN) * @param options.processInput - Custom function to process input arguments into attributes * @param options.processOutput - Custom function to process output values into attributes * @param options.attributes - Base attributes to be added to every span created * * @returns A wrapped function with identical signature that creates spans during execution * * @example * ```typescript * // Basic function wrapping * const add = (a: number, b: number) => a + b; * const tracedAdd = withSpan(add); * const result = tracedAdd(2, 3); // Creates a span named "add" * * // Async function with custom options * const fetchData = async (url: string) => { * const response = await fetch(url); * return response.json(); * }; * const tracedFetch = withSpan(fetchData, { * name: "api-request", * kind: "LLM" * }); * * // Custom input/output processing with base attributes * const processUser = (user: User) => ({ ...user, processed: true }); * const tracedProcess = withSpan(processUser, { * attributes: { * 'service.name': 'user-processor', * 'service.version': '1.0.0' * }, * processInput: (user) => ({ "user.id": user.id }), * processOutput: (result) => ({ "result.processed": result.processed }) * }); * ``` */ export function withSpan(fn: Fn, options?: SpanTraceOptions): Fn { const { tracer: _tracer, name: optionsName, processInput: _processInput, processOutput: _processOutput, openTelemetrySpanKind = SpanKind.INTERNAL, kind = OpenInferenceSpanKind.CHAIN, attributes: baseAttributes, } = options || {}; const configuredTracer: OITracer | undefined = _tracer ? wrapTracer(_tracer) : undefined; const processInput: InputToAttributesFn = _processInput ?? defaultProcessInput; const processOutput: OutputToAttributesFn = _processOutput ?? defaultProcessOutput; const spanName = optionsName || fn.name; const getErrorMessage = (error: unknown) => { if (typeof error === "object" && error !== null && "message" in error) { return String(error.message); } return String(error); }; // TODO: infer the name from the target const wrappedFn: Fn = function (this: ThisParameterType, ...args: Parameters) { const tracer = configuredTracer ?? getTracer(); return tracer.startActiveSpan( spanName, { kind: openTelemetrySpanKind, attributes: { ...baseAttributes, [OPENINFERENCE_SPAN_KIND]: kind, ...processInput(...args), }, }, (span) => { const recordError = (error: unknown) => { span.recordException(error as Error); span.setStatus({ code: SpanStatusCode.ERROR, message: getErrorMessage(error), }); }; try { const result = fn.apply(this, args) as ReturnType; if (isPromise(result)) { // Execute the promise and return the promise chain return result .then((value: Awaited>) => { span.setAttributes({ ...processOutput(value), }); span.setStatus({ code: SpanStatusCode.OK, }); return value; }) .catch((error: unknown) => { recordError(error); throw error; }) .finally(() => span.end()); } // It is a normal function span.setAttributes({ ...processOutput(result), }); span.setStatus({ code: SpanStatusCode.OK, }); span.end(); return result; } catch (error) { recordError(error); span.end(); throw error; } }, ); } as Fn; return wrappedFn; }