1+ import {
2+ SimpleSpanProcessor ,
3+ BatchSpanProcessor ,
4+ SpanProcessor ,
5+ ReadableSpan ,
6+ } from "@opentelemetry/sdk-trace-node" ;
7+ import { baggageUtils } from "@opentelemetry/core" ;
8+ import { Span , context } from "@opentelemetry/api" ;
9+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto" ;
10+ import { SpanExporter } from "@opentelemetry/sdk-trace-base" ;
11+ import {
12+ ASSOCATION_PROPERTIES_KEY ,
13+ ENTITY_NAME_KEY ,
14+ WORKFLOW_NAME_KEY ,
15+ } from "./tracing" ;
16+ import { SpanAttributes } from "@traceloop/ai-semantic-conventions" ;
17+
18+ export interface SpanProcessorOptions {
19+ /**
20+ * The API Key for sending traces data. Optional.
21+ * Defaults to the TRACELOOP_API_KEY environment variable.
22+ */
23+ apiKey ?: string ;
24+
25+ /**
26+ * The OTLP endpoint for sending traces data. Optional.
27+ * Defaults to TRACELOOP_BASE_URL environment variable or https://api.traceloop.com/
28+ */
29+ baseUrl ?: string ;
30+
31+ /**
32+ * Sends traces and spans without batching, for local development. Optional.
33+ * Defaults to false.
34+ */
35+ disableBatch ?: boolean ;
36+
37+ /**
38+ * The OpenTelemetry SpanExporter to be used for sending traces data. Optional.
39+ * Defaults to the OTLP exporter.
40+ */
41+ exporter ?: SpanExporter ;
42+
43+ /**
44+ * The headers to be sent with the traces data. Optional.
45+ */
46+ headers ?: Record < string , string > ;
47+ }
48+
49+ /**
50+ * Creates a span processor with Traceloop's custom span handling logic.
51+ * This can be used independently of the full SDK initialization.
52+ *
53+ * @param options - Configuration options for the span processor
54+ * @returns A configured SpanProcessor instance
55+ */
56+ export const createSpanProcessor = ( options : SpanProcessorOptions ) : SpanProcessor => {
57+ const headers =
58+ options . headers ||
59+ ( process . env . TRACELOOP_HEADERS
60+ ? baggageUtils . parseKeyPairsIntoRecord ( process . env . TRACELOOP_HEADERS )
61+ : { Authorization : `Bearer ${ options . apiKey } ` } ) ;
62+
63+ const traceExporter =
64+ options . exporter ??
65+ new OTLPTraceExporter ( {
66+ url : `${ options . baseUrl || process . env . TRACELOOP_BASE_URL || "https://api.traceloop.com" } /v1/traces` ,
67+ headers,
68+ } ) ;
69+
70+ const spanProcessor = options . disableBatch
71+ ? new SimpleSpanProcessor ( traceExporter )
72+ : new BatchSpanProcessor ( traceExporter ) ;
73+
74+ spanProcessor . onStart = ( span : Span ) => {
75+ const workflowName = context . active ( ) . getValue ( WORKFLOW_NAME_KEY ) ;
76+ if ( workflowName ) {
77+ span . setAttribute (
78+ SpanAttributes . TRACELOOP_WORKFLOW_NAME ,
79+ workflowName as string ,
80+ ) ;
81+ }
82+
83+ const entityName = context . active ( ) . getValue ( ENTITY_NAME_KEY ) ;
84+ if ( entityName ) {
85+ span . setAttribute (
86+ SpanAttributes . TRACELOOP_ENTITY_PATH ,
87+ entityName as string ,
88+ ) ;
89+ }
90+
91+ const associationProperties = context
92+ . active ( )
93+ . getValue ( ASSOCATION_PROPERTIES_KEY ) ;
94+ if ( associationProperties ) {
95+ for ( const [ key , value ] of Object . entries ( associationProperties ) ) {
96+ span . setAttribute (
97+ `${ SpanAttributes . TRACELOOP_ASSOCIATION_PROPERTIES } .${ key } ` ,
98+ value ,
99+ ) ;
100+ }
101+ }
102+ } ;
103+
104+ spanProcessor . onEnd = ( span : ReadableSpan ) => {
105+ // Vercel AI Adapters
106+ const attributes = span . attributes ;
107+
108+ // Adapt span names
109+ const nameMap : Record < string , string > = {
110+ "ai.generateText.doGenerate" : "ai.generateText.generate" ,
111+ "ai.streamText.doStream" : "ai.streamText.stream" ,
112+ } ;
113+ if ( span . name in nameMap ) {
114+ // Unfortunately, the span name is not writable as this is not the intended behavior
115+ // but it is a workaround to set the correct span name
116+ ( span as any ) . name = nameMap [ span . name ] ;
117+ }
118+
119+ if ( "ai.response.text" in attributes ) {
120+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.content` ] =
121+ attributes [ "ai.response.text" ] ;
122+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = "assistant" ;
123+ delete attributes [ "ai.response.text" ] ;
124+ }
125+
126+ if ( "ai.prompt.messages" in attributes ) {
127+ try {
128+ const messages = JSON . parse ( attributes [ "ai.prompt.messages" ] as string ) ;
129+ messages . forEach (
130+ ( msg : { role : string ; content : any } , index : number ) => {
131+ attributes [ `${ SpanAttributes . LLM_PROMPTS } .${ index } .content` ] =
132+ typeof msg . content === "string"
133+ ? msg . content
134+ : JSON . stringify ( msg . content ) ;
135+ attributes [ `${ SpanAttributes . LLM_PROMPTS } .${ index } .role` ] =
136+ msg . role ;
137+ } ,
138+ ) ;
139+ delete attributes [ "ai.prompt.messages" ] ;
140+ } catch ( e ) {
141+ //Skip if JSON parsing fails
142+ }
143+ }
144+
145+ if ( "ai.usage.promptTokens" in attributes ) {
146+ attributes [ `${ SpanAttributes . LLM_USAGE_PROMPT_TOKENS } ` ] =
147+ attributes [ "ai.usage.promptTokens" ] ;
148+ delete attributes [ "ai.usage.promptTokens" ] ;
149+ }
150+
151+ if ( "ai.usage.completionTokens" in attributes ) {
152+ attributes [ `${ SpanAttributes . LLM_USAGE_COMPLETION_TOKENS } ` ] =
153+ attributes [ "ai.usage.completionTokens" ] ;
154+ delete attributes [ "ai.usage.completionTokens" ] ;
155+ }
156+
157+ if (
158+ attributes [ `${ SpanAttributes . LLM_USAGE_PROMPT_TOKENS } ` ] &&
159+ attributes [ `${ SpanAttributes . LLM_USAGE_COMPLETION_TOKENS } ` ]
160+ ) {
161+ attributes [ `${ SpanAttributes . LLM_USAGE_TOTAL_TOKENS } ` ] =
162+ Number ( attributes [ `${ SpanAttributes . LLM_USAGE_PROMPT_TOKENS } ` ] ) +
163+ Number ( attributes [ `${ SpanAttributes . LLM_USAGE_COMPLETION_TOKENS } ` ] ) ;
164+ }
165+ } ;
166+
167+ // TODO: move this to the index.ts file
168+ // if (options.exporter) {
169+ // Telemetry.getInstance().capture("tracer:init", {
170+ // exporter: "custom",
171+ // processor: options.disableBatch ? "simple" : "batch",
172+ // });
173+ // } else {
174+ // Telemetry.getInstance().capture("tracer:init", {
175+ // exporter: options.baseUrl ?? "",
176+ // processor: options.disableBatch ? "simple" : "batch",
177+ // });
178+ // }
179+
180+ return spanProcessor ;
181+ }
0 commit comments