Skip to content

Commit 05a6326

Browse files
authored
feat(traceloop-sdk): standalone span processor (#596)
1 parent 446859c commit 05a6326

File tree

7 files changed

+8716
-4294
lines changed

7 files changed

+8716
-4294
lines changed

package-lock.json

+8,418-4,166
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sample-app/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"run:sample_vision": "npm run build && node dist/src/sample_vision_prompt.js",
2222
"run:sample_azure": "npm run build && node dist/src/sample_azure.js",
2323
"run:openai_streaming": "npm run build && node dist/src/sample_openai_streaming.js",
24+
"run:sample_otel_sdk": "npm run build && node dist/src/sample_otel_sdk.js",
2425
"run:sampler": "npm run build && node dist/src/sample_sampler.js",
2526
"run:llamaindex": "npm run build && node dist/src/sample_llamaindex.js",
2627
"run:llamaindex_openai_agent": "npm run build && node dist/src/sample_llama_index_openai_agent.js",
@@ -43,6 +44,7 @@
4344
"@google-cloud/aiplatform": "^3.34.0",
4445
"@google-cloud/vertexai": "^1.9.2",
4546
"@langchain/community": "^0.3.18",
47+
"@langchain/aws": "^0.1.8",
4648
"@llamaindex/openai": "^0.1.44",
4749
"@pinecone-database/pinecone": "^2.2.2",
4850
"@traceloop/node-server-sdk": "*",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ChatBedrockConverse } from "@langchain/aws";
2+
import * as traceloop from "@traceloop/node-server-sdk";
3+
4+
traceloop.initialize({
5+
appName: "sample_langchain_bedrock",
6+
apiKey: process.env.TRACELOOP_API_KEY,
7+
disableBatch: true,
8+
});
9+
10+
async function main() {
11+
const model = new ChatBedrockConverse({
12+
model: "anthropic.claude-3-haiku-20240307-v1:0",
13+
});
14+
15+
const response = await model.invoke("Tell me a joke about opentelemetry");
16+
console.log(response);
17+
}
18+
19+
void main().then(() => {
20+
console.log("Done");
21+
});
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { NodeSDK } from "@opentelemetry/sdk-node";
2+
import { Resource } from "@opentelemetry/resources";
3+
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
4+
import {
5+
createSpanProcessor,
6+
withTask,
7+
withWorkflow,
8+
} from "@traceloop/node-server-sdk";
9+
import { trace } from "@opentelemetry/api";
10+
import OpenAI from "openai";
11+
12+
const traceloopSpanProcessor = createSpanProcessor({
13+
apiKey: process.env.TRACELOOP_API_KEY,
14+
baseUrl: process.env.TRACELOOP_BASE_URL,
15+
disableBatch: true,
16+
});
17+
18+
// Initialize the OpenTelemetry SDK with Traceloop's span processor
19+
const sdk = new NodeSDK({
20+
resource: new Resource({
21+
[SemanticResourceAttributes.SERVICE_NAME]: "my-sample-app",
22+
}),
23+
spanProcessors: [traceloopSpanProcessor],
24+
});
25+
const openai = new OpenAI();
26+
27+
sdk.start();
28+
29+
async function main() {
30+
const tracer = trace.getTracer("my-sample-app");
31+
32+
return tracer.startActiveSpan("main.method", async (span) => {
33+
try {
34+
const chatResponse = await chat();
35+
console.log(chatResponse);
36+
return chatResponse;
37+
} catch (error) {
38+
span.recordException(error);
39+
throw error;
40+
} finally {
41+
span.end();
42+
}
43+
});
44+
}
45+
46+
async function chat() {
47+
return await withWorkflow({ name: "sample_chat" }, async () => {
48+
return await withTask({ name: "parent_task" }, async () => {
49+
return await withTask({ name: "child_task" }, async () => {
50+
const chatCompletion = await openai.chat.completions.create({
51+
messages: [
52+
{ role: "user", content: "Tell me a joke about OpenTelemetry" },
53+
],
54+
model: "gpt-3.5-turbo",
55+
logprobs: true,
56+
});
57+
58+
return chatCompletion.choices[0].message.content;
59+
});
60+
});
61+
});
62+
}
63+
64+
main()
65+
.then(() => {
66+
sdk
67+
.shutdown()
68+
.catch((error) => console.log("Error terminating application", error))
69+
.finally(() => process.exit(0));
70+
})
71+
.catch((error) => {
72+
console.error(error);
73+
process.exit(1);
74+
});

packages/traceloop-sdk/src/lib/node-server-sdk.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from "./tracing/decorators";
1313
export * from "./tracing/manual";
1414
export * from "./tracing/association";
1515
export * from "./tracing/custom-metric";
16+
export * from "./tracing/span-processor";
1617
export * from "./prompts";
1718

1819
initInstrumentations();

packages/traceloop-sdk/src/lib/tracing/index.ts

+13-128
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import { NodeSDK } from "@opentelemetry/sdk-node";
2-
import {
3-
SimpleSpanProcessor,
4-
BatchSpanProcessor,
5-
SpanProcessor,
6-
ReadableSpan,
7-
} from "@opentelemetry/sdk-trace-node";
2+
import { SpanProcessor } from "@opentelemetry/sdk-trace-node";
83
import { baggageUtils } from "@opentelemetry/core";
9-
import { Span, context, diag } from "@opentelemetry/api";
4+
import { context, diag } from "@opentelemetry/api";
105
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
116
import { Resource } from "@opentelemetry/resources";
127
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
138
import { Instrumentation } from "@opentelemetry/instrumentation";
149
import { InitializeOptions } from "../interfaces";
15-
import {
16-
ASSOCATION_PROPERTIES_KEY,
17-
ENTITY_NAME_KEY,
18-
WORKFLOW_NAME_KEY,
19-
} from "./tracing";
2010
import { Telemetry } from "../telemetry/telemetry";
2111
import { _configuration } from "../configuration";
22-
import {
23-
CONTEXT_KEY_ALLOW_TRACE_CONTENT,
24-
SpanAttributes,
25-
} from "@traceloop/ai-semantic-conventions";
12+
import { CONTEXT_KEY_ALLOW_TRACE_CONTENT } from "@traceloop/ai-semantic-conventions";
2613
import { AnthropicInstrumentation } from "@traceloop/instrumentation-anthropic";
2714
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
2815
import { AzureOpenAIInstrumentation } from "@traceloop/instrumentation-azure";
@@ -38,9 +25,10 @@ import { LangChainInstrumentation } from "@traceloop/instrumentation-langchain";
3825
import { ChromaDBInstrumentation } from "@traceloop/instrumentation-chromadb";
3926
import { QdrantInstrumentation } from "@traceloop/instrumentation-qdrant";
4027
import { TogetherInstrumentation } from "@traceloop/instrumentation-together";
28+
import { createSpanProcessor } from "./span-processor";
4129

4230
let _sdk: NodeSDK;
43-
let _spanProcessor: SimpleSpanProcessor | BatchSpanProcessor;
31+
let _spanProcessor: SpanProcessor;
4432
let openAIInstrumentation: OpenAIInstrumentation | undefined;
4533
let anthropicInstrumentation: AnthropicInstrumentation | undefined;
4634
let azureOpenAIInstrumentation: AzureOpenAIInstrumentation | undefined;
@@ -273,117 +261,14 @@ export const startTracing = (options: InitializeOptions) => {
273261
url: `${options.baseUrl}/v1/traces`,
274262
headers,
275263
});
276-
_spanProcessor = options.disableBatch
277-
? new SimpleSpanProcessor(traceExporter)
278-
: new BatchSpanProcessor(traceExporter);
279-
280-
_spanProcessor.onStart = (span: Span) => {
281-
const workflowName = context.active().getValue(WORKFLOW_NAME_KEY);
282-
if (workflowName) {
283-
span.setAttribute(
284-
SpanAttributes.TRACELOOP_WORKFLOW_NAME,
285-
workflowName as string,
286-
);
287-
}
288-
289-
const entityName = context.active().getValue(ENTITY_NAME_KEY);
290-
if (entityName) {
291-
span.setAttribute(
292-
SpanAttributes.TRACELOOP_ENTITY_PATH,
293-
entityName as string,
294-
);
295-
}
296-
297-
const associationProperties = context
298-
.active()
299-
.getValue(ASSOCATION_PROPERTIES_KEY);
300-
if (associationProperties) {
301-
for (const [key, value] of Object.entries(associationProperties)) {
302-
span.setAttribute(
303-
`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`,
304-
value,
305-
);
306-
}
307-
}
308-
};
309-
310-
const originalOnEnd = _spanProcessor.onEnd?.bind(_spanProcessor);
311-
_spanProcessor.onEnd = (span: ReadableSpan) => {
312-
// Vercel AI Adapters
313-
const attributes = span.attributes;
314-
315-
// Adapt span names
316-
const nameMap: Record<string, string> = {
317-
"ai.generateText.doGenerate": "ai.generateText.generate",
318-
"ai.streamText.doStream": "ai.streamText.stream",
319-
};
320-
if (span.name in nameMap) {
321-
// Unfortuantely, the span name is not writable as this is not the intended behavior
322-
// but it is a workaround to set the correct span name
323-
(span as any).name = nameMap[span.name];
324-
}
325-
326-
if ("ai.response.text" in attributes) {
327-
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] =
328-
attributes["ai.response.text"];
329-
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant";
330-
delete attributes["ai.response.text"];
331-
}
332-
333-
if ("ai.prompt.messages" in attributes) {
334-
try {
335-
const messages = JSON.parse(attributes["ai.prompt.messages"] as string);
336-
messages.forEach(
337-
(msg: { role: string; content: any }, index: number) => {
338-
attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] =
339-
typeof msg.content === "string"
340-
? msg.content
341-
: JSON.stringify(msg.content);
342-
attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] =
343-
msg.role;
344-
},
345-
);
346-
delete attributes["ai.prompt.messages"];
347-
} catch (e) {
348-
//Skip if JSON parsing fails
349-
}
350-
}
351-
352-
if ("ai.usage.promptTokens" in attributes) {
353-
attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`] =
354-
attributes["ai.usage.promptTokens"];
355-
delete attributes["ai.usage.promptTokens"];
356-
}
357-
358-
if ("ai.usage.completionTokens" in attributes) {
359-
attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`] =
360-
attributes["ai.usage.completionTokens"];
361-
delete attributes["ai.usage.completionTokens"];
362-
}
363-
364-
if (
365-
attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`] &&
366-
attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]
367-
) {
368-
attributes[`${SpanAttributes.LLM_USAGE_TOTAL_TOKENS}`] =
369-
Number(attributes[`${SpanAttributes.LLM_USAGE_PROMPT_TOKENS}`]) +
370-
Number(attributes[`${SpanAttributes.LLM_USAGE_COMPLETION_TOKENS}`]);
371-
}
372-
373-
originalOnEnd?.(span);
374-
};
375-
376-
if (options.exporter) {
377-
Telemetry.getInstance().capture("tracer:init", {
378-
exporter: "custom",
379-
processor: options.disableBatch ? "simple" : "batch",
380-
});
381-
} else {
382-
Telemetry.getInstance().capture("tracer:init", {
383-
exporter: options.baseUrl ?? "",
384-
processor: options.disableBatch ? "simple" : "batch",
385-
});
386-
}
264+
265+
_spanProcessor = createSpanProcessor({
266+
apiKey: options.apiKey,
267+
baseUrl: options.baseUrl,
268+
disableBatch: options.disableBatch,
269+
exporter: traceExporter,
270+
headers,
271+
});
387272

388273
const spanProcessors: SpanProcessor[] = [_spanProcessor];
389274
if (options.processor) {

0 commit comments

Comments
 (0)