How to Capture and Annotate LangGraph Node Spans Despite OTel Context Propagation Issues
Aryan D. Hey Aryan! Glad forceFlush() fixed the annotation timing issue. To your follow-up — yes, you can annotate the LangGraph node span directly instead of creating a custom span. The challenge is that trace.getActiveSpan() doesn't work inside your node function because LangGraph's async scheduling breaks OTel context propagation from the OpenInference instrumentor. Here are two approaches: Option 1: Custom SpanProcessor (recommended) Register a SpanProcessor with your TracerProvider that captures span IDs as they're created. This sees every span the LangChain instrumentor produces:
import { SpanProcessor, ReadableSpan } from '@opentelemetry/sdk-trace-base';
class SpanIdCapture implements SpanProcessor {
private spanMap = new Map<string, string>();
onStart() {}
onEnd(span: ReadableSpan) {
this.spanMap.set(span.name, span.spanContext().spanId);
}
getSpanId(name: string) { return this.spanMap.get(name); }
forceFlush() { return Promise.resolve(); }
shutdown() { return Promise.resolve(); }
}
const capture = new SpanIdCapture();
tracerProvider.addSpanProcessor(capture);
// After your graph invocation completes:
const judgeSpanId = capture.getSpanId('YourJudgeNodeName');
// Use judgeSpanId as recordId in batchUpdateAnnotationsIf the node can run multiple times (e.g. in a loop), key by both span name and trace ID, or collect into an array. Option 2: If using Python with BaseCallbackHandler The trick is to call trace.get_current_span() inside the callback method (where the instrumentor has set the OTel context), not inside your node function:
from opentelemetry import trace
class SpanCaptureHandler(BaseCallbackHandler):
def __init__(self):
self.span_ids = {}
def on_chain_start(self, serialized, inputs, *, run_id, **kwargs):
span = trace.get_current_span()
if span and span.is_recording():
ctx = span.get_span_context()
name = serialized.get("name", "")
self.span_ids[name] = format(ctx.span_id, '016x')Then after invocation: handler.span_ids['your_judge_node'] gives you the recordId. A couple of reminders for the annotation call: Use the actual span start timestamp as startTime (not midnight) — annotations are resolved against a UTC-day partition, so the wrong day = silent miss Make sure recordId is lowercase hex, 16 chars (which it should be if sourced from OTel) Still call forceFlush() and add a short delay before annotating to ensure the span has landed in Arize Hope that helps!
