Skip to content

feat(otel): support trace composition via parent span linking #301

@christso

Description

@christso

Context

AgentV's OTel exporter creates self-contained traces — each agentv eval run starts a new root span. There is no way to attach an eval trace to an existing parent span from a CI/CD pipeline or orchestrator.

Braintrust's trace-claude-code plugin supports this via CC_PARENT_SPAN_ID / CC_ROOT_SPAN_ID environment variables.

Proposal

Use W3C TRACEPARENT environment variable for trace composition. This is the industry standard (W3C Trace Context) and works automatically with any OTel-instrumented pipeline.

# CI/CD pipeline sets TRACEPARENT before spawning eval
TRACEPARENT="00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
  agentv eval evals/test.yaml --export-otel --otel-backend langfuse

Implementation

In otel-exporter.ts, before creating the root span:

import { W3CTraceContextPropagator } from '@opentelemetry/core';

// In exportResult(), before startActiveSpan:
const propagator = new W3CTraceContextPropagator();
const traceparent = process.env.TRACEPARENT;
let parentCtx = api.ROOT_CONTEXT;

if (traceparent) {
  parentCtx = propagator.extract(api.ROOT_CONTEXT, {
    traceparent,
    tracestate: process.env.TRACESTATE ?? '',
  }, {
    get: (carrier, key) => carrier[key],
    keys: (carrier) => Object.keys(carrier),
  });
}

tracer.startActiveSpan('agentv.eval', { startTime: startHr }, parentCtx, (rootSpan) => {
  // ... existing export logic (unchanged)
});

TRACEPARENT format

version-traceid-parentspanid-traceflags
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
  • version: always 00
  • traceid: 32 hex chars (128-bit)
  • parentspanid: 16 hex chars (64-bit)
  • traceflags: 01 = sampled

No TRACEPARENT → no change

When TRACEPARENT is not set, behavior is identical to today (new root trace). Zero config change required.

Files to modify

  1. packages/core/src/observability/otel-exporter.ts — Extract TRACEPARENT/TRACESTATE, create parent context
  2. packages/core/package.json@opentelemetry/core may already be a transitive dep; verify W3CTraceContextPropagator is available
  3. Tests — Verify child span has correct traceId and parentSpanId when TRACEPARENT is set

Acceptance criteria

  • When TRACEPARENT env var is set, the root eval span's traceId matches the provided trace ID
  • When TRACEPARENT env var is set, the root eval span's parentSpanId matches the provided span ID
  • When TRACEPARENT is not set, behavior is unchanged (new root trace)
  • TRACESTATE is propagated when present
  • Works with --otel-backend langfuse and --otel-backend braintrust

Use cases

  1. CI/CD correlation: GitHub Actions workflow runs multiple eval suites — all traces appear under the same pipeline span
  2. Multi-agent orchestration: An orchestrator launches agentv eval as a subtask — the eval trace nests under the orchestrator's span

References

Testing Approach

Unit Tests (InMemorySpanExporter)

const exporter = new InMemorySpanExporter();

// Simulate TRACEPARENT env var
process.env.TRACEPARENT = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01';

// Run eval with OTel export
// ...

const spans = exporter.getFinishedSpans();
const root = spans.find(s => s.name === 'gen_ai.eval');
const parentCtx = root.parentSpanId;
expect(parentCtx).toBe('b7ad6b7169203331');
expect(root.spanContext().traceId).toBe('0af7651916cd43dd8448eb211c80319c');

// Cleanup
delete process.env.TRACEPARENT;

Integration Test

# Verify trace shows up as child of CI span in Jaeger
docker run -d -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
TRACEPARENT='00-abc123...-def456...-01' agentv eval ... --export-otel
# Open Jaeger — verify eval span appears under the parent trace ID

What to Assert

  • When TRACEPARENT set: root span has correct traceId and parentSpanId
  • When TRACEPARENT not set: root span has no parent (standalone trace)
  • When TRACEPARENT malformed: falls back to standalone trace (no crash)
  • Trace ID propagates to all child spans

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions