Prompt Chain

Trace LLM Operations with OpenTelemetry

Promptfoo example instrumenting a custom LLM provider with OpenTelemetry and verifying traces via trace-based assertions.

Works with javascriptopentelemetry

92
Spark score
out of 100
Updated last month
Version code-scan-action-0.1

Add to Favorites

Why it matters

Integrate OpenTelemetry into your Javascript Promptfoo evaluations to gain visibility into LLM provider operations. Understand and debug internal processes for more robust AI-driven applications.

Outcomes

What it gets done

01

Instrument Javascript code for LLM evaluations.

02

Trace internal LLM provider operations.

03

Debug and optimize AI application performance.

04

Enhance Promptfoo evaluation observability.

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/pfoo-javascript | bash

Steps

Steps in the chain

01
OTLP receiver starts automatically
02
Promptfoo generates a trace context
03
Trace context passed to providers
04
Providers create child spans
05
Traces sent to OTLP endpoint
06
Promptfoo correlates traces

Overview

Javascript

A promptfoo example demonstrating OpenTelemetry instrumentation of a custom LLM provider, with trace-based and tool-call trajectory assertions verified through the promptfoo Trace Timeline UI. Use when building a custom provider whose internal operations you want traced and asserted on directly, rather than validating only the final output text.

What it does

This example shows how to use OpenTelemetry to trace the internal operations of a custom LLM provider during promptfoo evaluations, using standard OpenTelemetry SDKs rather than a promptfoo-specific tracing API. It requires no provider API keys since it uses a simulated RAG provider built purely to demonstrate the tracing pattern. Promptfoo's OTLP receiver starts automatically before evaluations begin, generates a trace context per test case, and passes that context to the provider via a traceparent field; the provider creates child spans with standard OpenTelemetry SDKs, sends them to promptfoo's OTLP endpoint (port 4318 by default), and promptfoo correlates the resulting traces back to their evaluations.

When to use - and when NOT to

Use this example when you're building a custom provider and want to instrument its internal operations (retrieval steps, tool calls, sub-requests) so they show up as spans correlated to specific promptfoo test cases, or when you want to assert on trace shape itself - span counts, span durations, error spans, or full tool-call trajectories - instead of just the final output text. It is not a general-purpose OpenTelemetry tutorial; it documents specifically how promptfoo's OTLP receiver, traceparent propagation, and trace-based assertions fit together.

Inputs and outputs

Scaffold and run the example with the quick start commands, then use the trajectory config variant to exercise tool-call assertions:

npx promptfoo@latest init --example integration-opentelemetry/javascript
cd integration-opentelemetry/javascript
npm install
npx promptfoo@latest eval
npx promptfoo@latest view

Tracing is enabled in promptfooconfig.yaml with tracing.enabled: true and an otlp.http block (enabled: true, port: 4318, host: '0.0.0.0'). The example ships four files: promptfooconfig.yaml (eval config with tracing and assertions), provider-simple-traced.js (a simulated RAG provider with comprehensive tracing), trace-assertions.js (a custom JavaScript trace-validation assertion), and package.json (OpenTelemetry v2.x dependencies). The provider parses the traceparent field from promptfoo's context to build a parent span context, then creates and ends its own child span around the provider logic, recording exceptions and status codes. After running an eval, npx promptfoo@latest view shows a Trace Timeline per test result with a hierarchical span visualization, duration bars, OK/ERROR status indicators, and span attributes/events. Trace-based assertion types include trace-span-count (min/max spans matching a name pattern), trace-span-duration (max duration in milliseconds for a matching span), and trace-error-spans (max allowed error-status spans). The trajectory config variant (promptfooconfig.trajectory.yaml) adds trajectory:tool-used, trajectory:tool-args-match, trajectory:tool-sequence, and trajectory:step-count assertions, and promptfoo recognizes both generic tool span attributes (tool.name, tool.arguments) and Vercel AI SDK telemetry attributes (ai.toolCall.name, ai.toolCall.args, ai.toolCall.arguments, ai.toolCall.input).

Integrations

Traces can be forwarded to Jaeger, Honeycomb, or any other OTLP-compatible backend by adding a forwarding block under tracing with an endpoint and optional auth headers. Standard OpenTelemetry environment variables also apply: OTEL_EXPORTER_OTLP_ENDPOINT for a custom collector endpoint, OTEL_EXPORTER_OTLP_HEADERS for collector auth headers, and PROMPTFOO_TRACING_ENABLED to toggle tracing without editing config. The example depends on OpenTelemetry v2.x packages: @opentelemetry/api (core tracing API), @opentelemetry/sdk-trace-node (Node.js tracer provider), @opentelemetry/exporter-trace-otlp-http (OTLP HTTP exporter), @opentelemetry/resources (resource attributes), and @opentelemetry/semantic-conventions (standard attribute names). A documented gotcha: if context.active is not a function appears, the OpenTelemetry context API is name-conflicting with promptfoo's own context parameter - rename the promptfoo parameter (e.g. to promptfooContext) to resolve it; if traces don't appear at all, verify tracing.enabled: true, confirm the OTLP receiver is listening on port 4318, check that promptfooContext.traceparent is being parsed, and call spanProcessor.forceFlush() before the provider function returns.

Who it's for

Developers building custom LLM providers who want their provider's internal operations traced with standard OpenTelemetry tooling and validated through promptfoo's trace-based and tool-call trajectory assertions, rather than relying on output-text assertions alone.

Source README

integration-opentelemetry/javascript (OpenTelemetry Tracing Example)

This example demonstrates how to use OpenTelemetry to trace the internal operations of your LLM providers during Promptfoo evaluations.

Quick Start

npx promptfoo@latest init --example integration-opentelemetry/javascript
cd integration-opentelemetry/javascript
npm install
npx promptfoo@latest eval
npx promptfoo@latest view

To run the trajectory assertion variant from this directory, use:

npx promptfoo@latest eval -c promptfooconfig.trajectory.yaml --no-cache

Environment Variables

This example requires no API keys - it uses a simulated provider that demonstrates tracing patterns.

Overview

Promptfoo's OpenTelemetry integration allows you to:

  • Trace internal operations of your providers without a custom SDK
  • Use standard OpenTelemetry libraries in any language
  • Send traces to any OpenTelemetry-compatible backend
  • Correlate traces with specific test cases and evaluations

How It Works

  1. OTLP receiver starts automatically - Promptfoo ensures the receiver is ready before evaluations begin
  2. Promptfoo generates a trace context for each test case evaluation
  3. The trace context is passed to providers via the traceparent field
  4. Providers create child spans using standard OpenTelemetry SDKs
  5. Traces are sent to Promptfoo's OTLP endpoint (port 4318 by default)
  6. Promptfoo correlates traces with evaluations for analysis

Files in This Example

File Description
promptfooconfig.yaml Evaluation config with tracing enabled and assertions
provider-simple-traced.js Simulated RAG provider with comprehensive tracing
trace-assertions.js Custom JavaScript assertion for trace validation
package.json OpenTelemetry dependencies (v2.x API)

Tracing Configuration

Enable tracing in your promptfooconfig.yaml:

tracing:
  enabled: true
  otlp:
    http:
      enabled: true
      port: 4318
      host: '0.0.0.0'

Instrumenting Your Provider

The provider receives trace context from Promptfoo via the traceparent field. Here's the pattern used in this example:

const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node');
const { resourceFromAttributes } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');

// Initialize OpenTelemetry (v2.x API)
const exporter = new OTLPTraceExporter({
  url: 'http://localhost:4318/v1/traces',
});

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'my-provider',
  }),
  spanProcessors: [new BatchSpanProcessor(exporter)],
});
provider.register();

const tracer = trace.getTracer('my-provider');

module.exports = {
  async callApi(prompt, promptfooContext) {
    // Parse trace context from Promptfoo
    if (promptfooContext?.traceparent) {
      const matches = promptfooContext.traceparent.match(
        /^(\d{2})-([a-f0-9]{32})-([a-f0-9]{16})-(\d{2})$/,
      );
      if (matches) {
        const [, , traceId, parentId, traceFlags] = matches;

        // Create parent context
        const parentCtx = trace.setSpanContext(context.active(), {
          traceId,
          spanId: parentId,
          traceFlags: parseInt(traceFlags, 16),
          isRemote: true,
        });

        // Run operations within parent context
        return context.with(parentCtx, async () => {
          const span = tracer.startSpan('my_operation');
          try {
            // Your provider logic here...
            span.setStatus({ code: SpanStatusCode.OK });
            return { output: 'result' };
          } catch (error) {
            span.recordException(error);
            span.setStatus({ code: SpanStatusCode.ERROR });
            throw error;
          } finally {
            span.end();
          }
        });
      }
    }

    return { output: 'result without tracing' };
  },
};

Trace-Based Assertions

This example demonstrates several trace assertion types:

assert:
  # Count spans matching a pattern
  - type: trace-span-count
    value:
      pattern: 'retrieve_document_*'
      min: 3
      max: 3

  # Check span duration
  - type: trace-span-duration
    value:
      pattern: 'rag_agent_workflow'
      max: 5000 # milliseconds

  # Check for error spans
  - type: trace-error-spans
    value:
      max_count: 0

The trajectory-specific config at promptfooconfig.trajectory.yaml adds:

  • trajectory:tool-used
  • trajectory:tool-args-match
  • trajectory:tool-sequence
  • trajectory:step-count

Promptfoo accepts generic tool span attributes such as tool.name and tool.arguments, and it also recognizes Vercel AI SDK telemetry attributes such as ai.toolCall.name, ai.toolCall.args, ai.toolCall.arguments, and ai.toolCall.input.

Viewing Traces

After running an evaluation, view traces in the web UI:

npx promptfoo@latest view

Click on any test result to see the "Trace Timeline" section showing:

  • Hierarchical span visualization
  • Duration bars showing relative timing
  • Status indicators (OK/ERROR)
  • Span attributes and events

Environment Variables

Configure OpenTelemetry using standard environment variables:

### Custom endpoint (defaults to Promptfoo's receiver)
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"

### Headers for authentication with external collectors
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key"

### Enable tracing via environment variable
export PROMPTFOO_TRACING_ENABLED=true

Forward to External Collectors

Send traces to Jaeger, Honeycomb, or other OTLP-compatible backends:

tracing:
  enabled: true
  forwarding:
    enabled: true
    endpoint: 'http://jaeger:4318'
    headers:
      'api-key': '${JAEGER_API_KEY}'

Troubleshooting

Context Naming Conflicts

If you see context.active is not a function, the OpenTelemetry context API conflicts with Promptfoo's context parameter. Rename the parameter:

async callApi(prompt, promptfooContext) {
  // Use promptfooContext for Promptfoo's context
  // Use context from @opentelemetry/api for tracing
}

Traces Not Appearing

  1. Verify tracing.enabled: true in config
  2. Check OTLP receiver is running (look for port 4318 in logs)
  3. Ensure trace context is properly parsed from promptfooContext.traceparent
  4. Call spanProcessor.forceFlush() before returning from provider

Dependencies

This example uses OpenTelemetry v2.x packages:

Package Version Purpose
@opentelemetry/api ^1.9.0 Core tracing API
@opentelemetry/sdk-trace-node ^2.0.0 Node.js tracer provider
@opentelemetry/exporter-trace-otlp-http ^0.200.0 OTLP HTTP exporter
@opentelemetry/resources ^2.0.0 Resource attributes
@opentelemetry/semantic-conventions ^1.28.0 Standard attribute names

FAQ

Common questions

Discussion

Questions & comments ยท 0

Sign In Sign in to leave a comment.