Skip to content

[Bug]: OtelTracingMiddleware produces orphan spans — child spans missing parent due to wrong OTel Context lookup #1938

Description

@Buktal

Describe the bug

OtelTracingMiddleware.onModelCall and onActing use Context.current() (ThreadLocal) to resolve the parent span when building child spans. Inside a Reactor pipeline, the parent OTel Context is stored in the Reactor Context (via ContextPropagationOperator.runWithContext), not in ThreadLocal. As a result:

  1. Child spans (chat <model>, execute_tool <name>) cannot find the invoke_agent parent span and become orphan root spans in the backend (e.g. Langfuse).
  2. Any SpanProcessor.onStart that reads the parent Context (e.g. to inject business attributes) receives an empty context and cannot propagate biz metadata.

The correct approach is to use Flux.deferContextual and read the parent context via ContextPropagationOperator.getOpenTelemetryContextFromContextView(ctxView, Context.current()), then explicitly call spanBuilder.setParent(parentContext). This is exactly the pattern used in the now-deprecated TelemetryTracer.

To Reproduce

ReActAgent agent = ReActAgent.builder()
    .name("assistant")
    .model(model)
    .middleware(new OtelTracingMiddleware())
    .build();

Configure any OTLP-compatible backend (e.g. Langfuse). Invoke the agent. Observe in the trace UI:

  • Only the invoke_agent span appears at the top level.
  • chat <model> and execute_tool <name> spans appear as separate root spans (no parent), or are missing entirely.

Expected behavior

chat <model> and execute_tool <name> spans should be nested under invoke_agent as child spans, forming a complete trace tree.

Error messages

No exception. Silent behavior — orphan spans appear at the OTLP backend instead of a nested hierarchy.

Root cause

onModelCall (and onActing) use Flux.defer + Context.current():

// Current (broken)
return Flux.defer(() -> {
    Span span = getTracer()
        .spanBuilder("chat " + modelName)
        // ...
        .startSpan();
    Context otelCtx = Context.current().with(span);  // ThreadLocal — empty in Reactor pipeline
    ...
});

Should be:

// Fixed
return Flux.deferContextual(ctxView -> {
    Context parentContext = ContextPropagationOperator
        .getOpenTelemetryContextFromContextView(ctxView, Context.current());
    Span span = getTracer()
        .spanBuilder("chat " + modelName)
        // ...
        .setParent(parentContext)
        .startSpan();
    Context otelCtx = parentContext.with(span);
    ...
});

Note: onAgent already uses ContextPropagationOperator.runWithContext to propagate the span into Reactor Context, so the parent context is available — it just needs to be read correctly by the child hooks.

Environment

  • AgentScope-Java Version: 2.0.0-RC4
  • Java Version: 21
  • OS: Windows 11

Additional context

The deprecated TelemetryTracer resolved this correctly via ContextPropagationOperator.getOpenTelemetryContextFromContextView. The same fix pattern should be applied to all three middleware hooks in OtelTracingMiddleware.

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