From b492f728c0a84a00d2b8b2239b7c989281d500b7 Mon Sep 17 00:00:00 2001 From: dbrian57 Date: Wed, 14 Jan 2026 15:23:11 -0500 Subject: [PATCH] Adds OTEL Typescript examples --- weave/guides/tracking/otel.mdx | 665 ++++++++++++++++++++++++++++++++- 1 file changed, 651 insertions(+), 14 deletions(-) diff --git a/weave/guides/tracking/otel.mdx b/weave/guides/tracking/otel.mdx index 4ebb737136..37fdf05d2b 100644 --- a/weave/guides/tracking/otel.mdx +++ b/weave/guides/tracking/otel.mdx @@ -28,29 +28,48 @@ Standard W&B authentication is used. You must have write permissions to the proj - `project_id: /` - `Authorization=Basic ` -## Examples: +## Examples + +The following examples show how to send OpenTelemetry traces to Weave using Python and TypeScript. + +Before running the code samples below, set the following fields: -You must modify the following fields before you can run the code samples below: 1. `WANDB_API_KEY`: You can get this from [User Settings](https://wandb.ai/settings). -2. Entity: You can only log traces to the project under an entity that you have access to. You can find your entity name by visiting your W&N dashboard at [https://wandb.ai/home], and checking the **Teams** field in the left sidebar. +2. Entity: You can only log traces to the project under an entity that you have access to. You can find your entity name by visiting your W&B dashboard at [https://wandb.ai/home], and checking the **Teams** field in the left sidebar. 3. Project Name: Choose a fun name! 4. `OPENAI_API_KEY`: You can obtain this from the [OpenAI dashboard](https://platform.openai.com/api-keys). -### OpenInference Instrumentation: +### OpenInference Instrumentation This example shows how to use the OpenAI instrumentation. There are many more available which you can find in the official repository: https://github.com/Arize-ai/openinference First, install the required dependencies: + + + ```bash pip install openai openinference-instrumentation-openai opentelemetry-exporter-otlp-proto-http ``` + + + +```bash +npm install openai @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-proto @arizeai/openinference-instrumentation-openai @opentelemetry/api +``` + + + + **Performance Recommendation**: Always use `BatchSpanProcessor` instead of `SimpleSpanProcessor` when sending traces to Weave. `SimpleSpanProcessor` exports spans synchronously, potentially impacting the performance of other workloads. These examples illustrate `BatchSpanProcessor`, which is recommended in production because it batches spans asynchronously and efficiently. -Next, paste the following code into a python file such as `openinference_example.py` + + + +Paste the following code into a Python file such as `openinference_example.py`: ```python lines import base64 @@ -108,23 +127,140 @@ if __name__ == "__main__": main() ``` -Finally, once you have set the fields specified above to their correct values, run the code: +Run the code: ```bash python openinference_example.py ``` -### OpenLLMetry Instrumentation: + + +The TypeScript implementation of this example contains the following key differences from the Python implementation: + +* OpenAI must be imported before registering instrumentation (ESM modules require this). +* Uses @opentelemetry/exporter-trace-otlp-proto (protobuf format) instead of the HTTP exporter, since W&B's endpoint only accepts protobuf. +* Requires explicit `provider.shutdown()` with a delay before shutdown to ensure spans are flushed, since `BatchSpanProcessor` flushes asynchronously. + +Paste the following code into a TypeScript file such as `openinference_example.ts`: + +```typescript lines +// IMPORTANT: Import OpenAI FIRST so instrumentation can patch it +import OpenAI from "openai"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { OpenAIInstrumentation, isPatched } from "@arizeai/openinference-instrumentation-openai"; +import { trace } from "@opentelemetry/api"; + +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const WANDB_BASE_URL = "https://trace.wandb.ai"; +const PROJECT_ID = "dans-test-team/otel-test-python"; + +const OTEL_EXPORTER_OTLP_ENDPOINT = `${WANDB_BASE_URL}/otel/v1/traces`; + +// Create an API key at https://wandb.ai/settings +const WANDB_API_KEY = process.env.WANDB_API_KEY!; +const AUTH = Buffer.from(`api:${WANDB_API_KEY}`).toString("base64"); + +const OTEL_EXPORTER_OTLP_HEADERS = { + Authorization: `Basic ${AUTH}`, + project_id: PROJECT_ID, +}; + +// Configure the OTLP exporter +const exporter = new OTLPTraceExporter({ + url: OTEL_EXPORTER_OTLP_ENDPOINT, + headers: OTEL_EXPORTER_OTLP_HEADERS, +}); + +const provider = new NodeTracerProvider({ + spanProcessors: [ + new BatchSpanProcessor(exporter), + new BatchSpanProcessor(new ConsoleSpanExporter()), + ], +}); + +provider.register(); + +// Register the OpenAI instrumentation with the tracer provider +const openAIInstrumentation = new OpenAIInstrumentation(); +openAIInstrumentation.setTracerProvider(provider); + +// Manually instrument OpenAI since we're using ESM +openAIInstrumentation.manuallyInstrument(OpenAI); + +console.log("Instrumentation registered. Is patched?", isPatched()); + +async function main() { + console.log("OpenAI is patched?", isPatched()); + + // Create a manual span to verify pipeline works + const tracer = trace.getTracer("test-tracer"); + const manualSpan = tracer.startSpan("manual-test-span"); + manualSpan.setAttribute("test", "manual span works"); + manualSpan.end(); + console.log("Manual span created"); + + const client = new OpenAI({ apiKey: OPENAI_API_KEY }); + + // Using non-streaming first to test instrumentation + console.log("Making OpenAI API call..."); + const response = await client.chat.completions.create({ + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: "Describe OTEL in a single sentence." }], + max_tokens: 50, + }); + + console.log("Response:", response.choices[0]?.message?.content); + console.log("Waiting for spans to flush..."); +} + +await main(); + +// Give spans time to flush +console.log("Waiting 2 seconds for spans to flush..."); +await new Promise(resolve => setTimeout(resolve, 2000)); + +await provider.shutdown(); // flush all pending spans before exit +console.log("Shutdown complete"); +``` + +Run the code: + +```bash +npx ts-node openinference_example.ts +``` + + + + +### OpenLLMetry Instrumentation The following example shows how to use the OpenAI instrumentation. Additional examples are available at [https://github.com/traceloop/openllmetry/tree/main/packages](https://github.com/traceloop/openllmetry/tree/main/packages). -First install the required dependencies: +First, install the required dependencies: + + + ```bash pip install openai opentelemetry-instrumentation-openai opentelemetry-exporter-otlp-proto-http ``` -Next, paste the following code into a python file such as `openllmetry_example.py`. Note that this is the same code as above, except the `OpenAIInstrumentor` is imported from `opentelemetry.instrumentation.openai` instead of `openinference.instrumentation.openai` + + + +```bash +npm install openai @traceloop/instrumentation-openai @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-http +``` + + + + + + + +Paste the following code into a Python file such as `openllmetry_example.py`. Note that this is the same code as above, except the `OpenAIInstrumentor` is imported from `opentelemetry.instrumentation.openai` instead of `openinference.instrumentation.openai`: ```python lines import base64 @@ -182,23 +318,128 @@ if __name__ == "__main__": main() ``` -Finally, once you have set the fields specified above to their correct values, run the code: +Run the code: ```bash python openllmetry_example.py ``` + + + +Paste the following code into a TypeScript file such as `openllmetry_example.ts`. Note that this uses the Traceloop OpenAI instrumentation package: + +```typescript lines +import OpenAI from "openai"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; +import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai"; +import { registerInstrumentations } from "@opentelemetry/instrumentation"; + +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const WANDB_BASE_URL = "https://trace.wandb.ai"; +const PROJECT_ID = "dans-test-team/otel-test-python"; + +const OTEL_EXPORTER_OTLP_ENDPOINT = `${WANDB_BASE_URL}/otel/v1/traces`; + +// Create an API key at https://wandb.ai/settings +const WANDB_API_KEY = process.env.WANDB_API_KEY!; +const AUTH = Buffer.from(`api:${WANDB_API_KEY}`).toString("base64"); + +const OTEL_EXPORTER_OTLP_HEADERS = { + Authorization: `Basic ${AUTH}`, + project_id: PROJECT_ID, +}; + +// Configure the OTLP exporter +const exporter = new OTLPTraceExporter({ + url: OTEL_EXPORTER_OTLP_ENDPOINT, + headers: OTEL_EXPORTER_OTLP_HEADERS, +}); + +const provider = new NodeTracerProvider({ + spanProcessors: [ + new BatchSpanProcessor(exporter), + // Optionally, print the spans to the console. + new BatchSpanProcessor(new ConsoleSpanExporter()), + ], +}); + +provider.register(); + +// Register the OpenAI instrumentation with the tracer provider +const openAIInstrumentation = new OpenAIInstrumentation(); +registerInstrumentations({ + tracerProvider: provider, + instrumentations: [openAIInstrumentation], +}); + +// Manually instrument OpenAI since we're using ESM +openAIInstrumentation.manuallyInstrument(OpenAI); + +async function main() { + const client = new OpenAI({ apiKey: OPENAI_API_KEY }); + const stream = await client.chat.completions.create({ + model: "gpt-3.5-turbo", + messages: [{ role: "user", content: "Describe OTEL in a single sentence." }], + max_tokens: 20, + stream: true, + }); + + for await (const chunk of stream) { + const content = chunk.choices[0]?.delta?.content; + if (content) { + process.stdout.write(content); + } + } + console.log(); // newline after streaming +} + +await main(); + +// Give spans time to flush +await new Promise(resolve => setTimeout(resolve, 2000)); + +await provider.shutdown(); // flush all pending spans before exit +``` + +Run the code: + +```bash +npx ts-node openllmetry_example.ts +``` + + + + ### Without Instrumentation If you would prefer to use OTEL directly instead of an instrumentation package, you may do so. Span attributes will be parsed according to the OpenTelemetry semantic conventions described at [https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/). First, install the required dependencies: + + + ```bash pip install openai opentelemetry-sdk opentelemetry-api opentelemetry-exporter-otlp-proto-http ``` -Next, paste the following code into a python file such as `opentelemetry_example.py` + + + +```bash +npm install openai @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-http +``` + + + + + + + +Paste the following code into a Python file such as `opentelemetry_example.py`: ```python lines import json @@ -269,12 +510,106 @@ if __name__ == "__main__": my_function() ``` -Finally, once you have set the fields specified above to their correct values, run the code: +Run the code: ```bash python opentelemetry_example.py ``` + + + +Paste the following code into a TypeScript file such as `opentelemetry_example.ts`: + +```typescript lines +import OpenAI from "openai"; +import { trace } from "@opentelemetry/api"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; + +const OPENAI_API_KEY = "YOUR_OPENAI_API_KEY"; +const WANDB_BASE_URL = "https://trace.wandb.ai"; +const PROJECT_ID = "/"; + +const OTEL_EXPORTER_OTLP_ENDPOINT = `${WANDB_BASE_URL}/otel/v1/traces`; + +// Create an API key at https://wandb.ai/settings +const WANDB_API_KEY = ""; +const AUTH = Buffer.from(`api:${WANDB_API_KEY}`).toString("base64"); + +const OTEL_EXPORTER_OTLP_HEADERS = { + Authorization: `Basic ${AUTH}`, + project_id: PROJECT_ID, +}; + +const provider = new NodeTracerProvider(); + +// Configure the OTLP exporter +const exporter = new OTLPTraceExporter({ + url: OTEL_EXPORTER_OTLP_ENDPOINT, + headers: OTEL_EXPORTER_OTLP_HEADERS, +}); + +// Add the exporter to the tracer provider +provider.addSpanProcessor(new BatchSpanProcessor(exporter)); + +// Optionally, print the spans to the console. +provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter())); + +provider.register(); + +// Creates a tracer from the global tracer provider +const tracer = trace.getTracer("my-app"); + +async function myFunction() { + const span = tracer.startSpan("outer_span"); + + try { + const client = new OpenAI({ apiKey: OPENAI_API_KEY }); + const inputMessages = [ + { role: "user" as const, content: "Describe OTEL in a single sentence." }, + ]; + + // This will only appear in the side panel + span.setAttribute("input.value", JSON.stringify(inputMessages)); + // This follows conventions and will appear in the dashboard + span.setAttribute("gen_ai.system", "openai"); + + const stream = await client.chat.completions.create({ + model: "gpt-3.5-turbo", + messages: inputMessages, + max_tokens: 20, + stream: true, + }); + + let output = ""; + for await (const chunk of stream) { + const content = chunk.choices[0]?.delta?.content; + if (content) { + output += content; + } + } + + // This will only appear in the side panel + span.setAttribute("output.value", JSON.stringify({ content: output })); + } finally { + span.end(); + } +} + +myFunction(); +``` + +Run the code: + +```bash +npx ts-node opentelemetry_example.ts +``` + + + + The span attribute prefixes `gen_ai` and `openinference` are used to determine which convention to use, if any, when interpreting the trace. If neither key is detected, then all span attributes are visible in the trace view. The full span is available in the side panel when you select a trace. ## Organize OTEL traces into threads @@ -286,13 +621,16 @@ Add the following attributes to your OTEL spans to enable thread grouping: - `wandb.thread_id`: Groups spans into a specific thread - `wandb.is_turn`: Marks a span as a conversation turn (appears as a row in the thread view) -The following code shows several examples of organizing OTEL traces into Weave threads. They use `wandb.thread_id` to group related operations, and use `wandb.is_turn` to view high level operations as rows in the thread view. Each example performs the followingmark high-level operations that appear as rows in the thread view). +The following examples show how to organize OTEL traces into Weave threads. They use `wandb.thread_id` to group related operations and `wandb.is_turn` to mark high-level operations that appear as rows in the thread view. Use this configuration to run these examples: + + + ```python lines import base64 import json @@ -337,9 +675,71 @@ trace.set_tracer_provider(tracer_provider) # Create a tracer from the global tracer provider tracer = trace.get_tracer(__name__) ``` + + + + +```typescript lines +import { trace, context } from "@opentelemetry/api"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { + BatchSpanProcessor, + ConsoleSpanExporter, +} from "@opentelemetry/sdk-trace-base"; +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; + + +// Configuration - Update these values to your own W&B entity and project name +const ENTITY = "dans-test-team"; +const PROJECT = "otel-test-typescript"; +const PROJECT_ID = `${ENTITY}/${PROJECT}`; +const WANDB_API_KEY = process.env.WANDB_API_KEY; + +if (!WANDB_API_KEY) { + console.error("Error: WANDB_API_KEY environment variable is not set"); + console.error("Run: export WANDB_API_KEY=your_api_key_here"); + process.exit(1); +} + +// OTEL Setup +const OTEL_EXPORTER_OTLP_ENDPOINT = "https://trace.wandb.ai/otel/v1/traces"; +const AUTH = Buffer.from(`api:${WANDB_API_KEY}`).toString("base64"); +const OTEL_EXPORTER_OTLP_HEADERS = { + Authorization: `Basic ${AUTH}`, + project_id: PROJECT_ID, +}; + +// Configure the OTLP exporter +const exporter = new OTLPTraceExporter({ + url: OTEL_EXPORTER_OTLP_ENDPOINT, + headers: OTEL_EXPORTER_OTLP_HEADERS, +}); + +// Initialize tracer provider with span processors +const provider = new NodeTracerProvider({ + spanProcessors: [ + new BatchSpanProcessor(exporter), + new BatchSpanProcessor(new ConsoleSpanExporter()), + ], +}); + +// Register the tracer provider +provider.register(); + +// Create a tracer from the global tracer provider +const tracer = trace.getTracer("threads-examples"); +``` + + + + + + + + ```python lines def example_1_basic_thread_and_turn(): """Example 1: Basic thread with a single turn""" @@ -370,13 +770,67 @@ def example_1_basic_thread_and_turn(): print(f"Turn completed in thread: {thread_id}") def main(): - example_1_basic_thread_and_turn + + +```typescript lines +function example_1_basic_thread_and_turn() { + console.log("\n=== Example 1: Basic Thread and Turn ==="); + + // Create a thread context + const threadId = "thread_example_1"; + + // This span represents a turn (direct child of thread) + tracer.startActiveSpan("process_user_message", (turnSpan) => { + // Set thread attributes + turnSpan.setAttribute("wandb.thread_id", threadId); + turnSpan.setAttribute("wandb.is_turn", true); + + // Add some example attributes + turnSpan.setAttribute("input.value", "Hello, help me with setup"); + + let response: string; + + // Simulate some work with nested spans + tracer.startActiveSpan("generate_response", (nestedSpan) => { + // This is a nested call within the turn, so is_turn should be false or unset + nestedSpan.setAttribute("wandb.thread_id", threadId); + // wandb.is_turn is not set or set to false for nested calls + + response = "I'll help you get started with the setup process."; + nestedSpan.setAttribute("output.value", response); + nestedSpan.end(); + }); + + turnSpan.setAttribute("output.value", response!); + console.log(`Turn completed in thread: ${threadId}`); + turnSpan.end(); + }); +} + +function main() { + example_1_basic_thread_and_turn(); +} + +main(); +``` + + + + + + + + ```python lines def example_2_multiple_turns(): """Example 2: Multiple turns in a single thread""" @@ -416,12 +870,81 @@ def example_2_multiple_turns(): def main(): example_2_multiple_turns() + if __name__ == "__main__": main() ``` + + + + +```typescript lines +function example_2_multiple_turns() { + console.log("\n=== Example 2: Multiple Turns in Thread ==="); + + const threadId = "thread_conversation_123"; + + // Turn 1 + tracer.startActiveSpan("process_message_turn1", (turn1Span) => { + turn1Span.setAttribute("wandb.thread_id", threadId); + turn1Span.setAttribute("wandb.is_turn", true); + turn1Span.setAttribute( + "input.value", + "What programming languages do you recommend?" + ); + + // Nested operations + tracer.startActiveSpan("analyze_query", (analyzeSpan) => { + analyzeSpan.setAttribute("wandb.thread_id", threadId); + // No is_turn attribute or set to false for nested spans + analyzeSpan.end(); + }); + + const response1 = + "I recommend Python for beginners and JavaScript for web development."; + turn1Span.setAttribute("output.value", response1); + console.log(`Turn 1 completed in thread: ${threadId}`); + turn1Span.end(); + }); + + // Turn 2 + tracer.startActiveSpan("process_message_turn2", (turn2Span) => { + turn2Span.setAttribute("wandb.thread_id", threadId); + turn2Span.setAttribute("wandb.is_turn", true); + turn2Span.setAttribute("input.value", "Can you explain Python vs JavaScript?"); + + // Nested operations + tracer.startActiveSpan("comparison_analysis", (compareSpan) => { + compareSpan.setAttribute("wandb.thread_id", threadId); + compareSpan.setAttribute("wandb.is_turn", false); // Explicitly false for nested + compareSpan.end(); + }); + + const response2 = + "Python excels at data science while JavaScript dominates web development."; + turn2Span.setAttribute("output.value", response2); + console.log(`Turn 2 completed in thread: ${threadId}`); + turn2Span.end(); + }); +} + +function main() { + example_2_multiple_turns(); +} + +main(); +``` + + + + + + + + ```python lines def example_3_complex_nested_structure(): """Example 3: Complex nested structure with multiple levels""" @@ -461,13 +984,85 @@ def example_3_complex_nested_structure(): def main(): example_3_complex_nested_structure() + if __name__ == "__main__": main() ``` + + + + +```typescript lines +function example_3_complex_nested_structure() { + console.log("\n=== Example 3: Complex Nested Structure ==="); + + const threadId = "thread_complex_456"; + + // Turn with multiple levels of nesting + tracer.startActiveSpan("handle_complex_request", (turnSpan) => { + turnSpan.setAttribute("wandb.thread_id", threadId); + turnSpan.setAttribute("wandb.is_turn", true); + turnSpan.setAttribute( + "input.value", + "Analyze this code and suggest improvements" + ); + + // Level 1 nested operation + tracer.startActiveSpan("code_analysis", (analysisSpan) => { + analysisSpan.setAttribute("wandb.thread_id", threadId); + // No is_turn for nested operations + + // Level 2 nested operation + tracer.startActiveSpan("syntax_check", (syntaxSpan) => { + syntaxSpan.setAttribute("wandb.thread_id", threadId); + syntaxSpan.setAttribute("result", "No syntax errors found"); + syntaxSpan.end(); + }); + + // Another Level 2 nested operation + tracer.startActiveSpan("performance_check", (perfSpan) => { + perfSpan.setAttribute("wandb.thread_id", threadId); + perfSpan.setAttribute("result", "Found 2 optimization opportunities"); + perfSpan.end(); + }); + + analysisSpan.end(); + }); + + // Another Level 1 nested operation + tracer.startActiveSpan("generate_suggestions", (suggestSpan) => { + suggestSpan.setAttribute("wandb.thread_id", threadId); + const suggestions = ["Use list comprehension", "Consider caching results"]; + suggestSpan.setAttribute("suggestions", JSON.stringify(suggestions)); + suggestSpan.end(); + }); + + turnSpan.setAttribute( + "output.value", + "Analysis complete with 2 improvement suggestions" + ); + console.log(`Complex turn completed in thread: ${threadId}`); + turnSpan.end(); + }); +} + +function main() { + example_3_complex_nested_structure(); +} + +main(); +``` + + + + + + + ```python lines def example_4_non_turn_operations(): """Example 4: Operations that are part of a thread but not turns""" @@ -493,9 +1088,51 @@ def example_4_non_turn_operations(): def main(): example_4_non_turn_operations() + if __name__ == "__main__": main() ``` + + + + +```typescript lines +function example_4_non_turn_operations() { + console.log("\n=== Example 4: Non-Turn Thread Operations ==="); + + const threadId = "thread_background_789"; + + // Background operation that's part of thread but not a turn + tracer.startActiveSpan("background_indexing", (bgSpan) => { + bgSpan.setAttribute("wandb.thread_id", threadId); + // wandb.is_turn is unset or false - this is not a turn + bgSpan.setAttribute("wandb.is_turn", false); + bgSpan.setAttribute("operation", "Indexing conversation history"); + console.log(`Background operation in thread: ${threadId}`); + bgSpan.end(); + }); + + // Actual turn in the same thread + tracer.startActiveSpan("user_query", (turnSpan) => { + turnSpan.setAttribute("wandb.thread_id", threadId); + turnSpan.setAttribute("wandb.is_turn", true); + turnSpan.setAttribute("input.value", "Search my previous conversations"); + turnSpan.setAttribute("output.value", "Found 5 relevant conversations"); + console.log(`Turn completed in thread: ${threadId}`); + turnSpan.end(); + }); +} + +function main() { + example_4_non_turn_operations(); +} + +main(); +``` + + + + After sending these traces, you can view them in the Weave UI under the **Threads** tab, where they'll be grouped by `thread_id` and each turn will appear as a separate row.