Skip to content

Telemetry: Plugins observe incorrect current span #1252

@r4inee

Description

@r4inee

Describe the Bug:
ADK Java plugin callbacks are not consistently invoked with the semantically correct OpenTelemetry span as Span.current(). This affects plugin authors who enrich ADK-created spans using:

Span.current().setAttribute(...);

Steps to Reproduce:

  1. Use ADK Java with OpenTelemetry tracing enabled, and with either manual context propagation or via a library.
  2. Add the following plugin that logs Span.current() inside model and tool callbacks.
  3. Run a tool-calling agent with a prompt such as: “What is the weather in Sydney?”
  4. Compare callback Span.current() span IDs to exported span IDs.
public final class CurrentSpanDebugPlugin extends BasePlugin {
  public CurrentSpanDebugPlugin() {
    super("CurrentSpanDebugPlugin");
  }

  @Override
  public Maybe<LlmResponse> beforeModelCallback(
      CallbackContext callbackContext, LlmRequest.Builder llmRequest) {
    recordModel("beforeModelCallback", callbackContext);
    return Maybe.empty();
  }

  @Override
  public Maybe<LlmResponse> afterModelCallback(
      CallbackContext callbackContext, LlmResponse llmResponse) {
    recordModel(
        "afterModelCallback partial=" + llmResponse.partial().orElse(null),
        callbackContext);
    return Maybe.empty();
  }

  @Override
  public Maybe<LlmResponse> onModelErrorCallback(
      CallbackContext callbackContext,
      LlmRequest.Builder llmRequest,
      Throwable error) {
    recordModel("onModelErrorCallback error=" + error.getMessage(), callbackContext);
    return Maybe.empty();
  }

  @Override
  public Maybe<Map<String, Object>> beforeToolCallback(
      BaseTool tool, Map<String, Object> toolArgs, ToolContext toolContext) {
    recordTool("beforeToolCallback:" + tool.name(), toolContext);
    return Maybe.empty();
  }

  @Override
  public Maybe<Map<String, Object>> afterToolCallback(
      BaseTool tool,
      Map<String, Object> toolArgs,
      ToolContext toolContext,
      Map<String, Object> result) {
    recordTool("afterToolCallback:" + tool.name(), toolContext);
    return Maybe.empty();
  }

  @Override
  public Maybe<Map<String, Object>> onToolErrorCallback(
      BaseTool tool,
      Map<String, Object> toolArgs,
      ToolContext toolContext,
      Throwable error) {
    recordTool("onToolErrorCallback:" + tool.name() + " error=" + error.getMessage(), toolContext);
    return Maybe.empty();
  }

  private static void recordModel(String callback, CallbackContext callbackContext) {
    Span span = Span.current();
    SpanContext spanContext = span.getSpanContext();

    // Marker attributes show which exported span this callback mutated.
    span.setAttribute("debug.callback", callback);
    span.setAttribute("debug.callback.span_id", spanContext.getSpanId());
    span.setAttribute("debug.callback.trace_id", spanContext.getTraceId());
    span.setAttribute("debug.callback.valid", spanContext.isValid());
    span.setAttribute("debug.callback.invocation_id", callbackContext.invocationId());
    span.setAttribute("debug.callback.event_id", callbackContext.eventId());

    System.out.printf(
        "PLUGIN %s invocationId=%s eventId=%s currentSpanId=%s currentTraceId=%s valid=%s%n",
        callback,
        callbackContext.invocationId(),
        callbackContext.eventId(),
        spanContext.getSpanId(),
        spanContext.getTraceId(),
        spanContext.isValid());
  }

  private static void recordTool(String callback, ToolContext toolContext) {
    Span span = Span.current();
    SpanContext spanContext = span.getSpanContext();

    // Marker attributes show which exported span this callback mutated.
    span.setAttribute("debug.callback", callback);
    span.setAttribute("debug.callback.span_id", spanContext.getSpanId());
    span.setAttribute("debug.callback.trace_id", spanContext.getTraceId());
    span.setAttribute("debug.callback.valid", spanContext.isValid());
    span.setAttribute("debug.callback.agent_name", toolContext.agentName());
    span.setAttribute(
        "debug.callback.function_call_id",
        toolContext.functionCallId().orElse("<none>"));

    System.out.printf(
        "PLUGIN %s functionCallId=%s agentName=%s currentSpanId=%s currentTraceId=%s valid=%s%n",
        callback,
        toolContext.functionCallId().orElse("<none>"),
        toolContext.agentName(),
        spanContext.getSpanId(),
        spanContext.getTraceId(),
        spanContext.isValid());
  }
}

Expected Behavior:
Plugin callbacks should observe the span corresponding to their semantic operation.

beforeModelCallback #1 -> first call_llm
afterModelCallback  #1 -> first call_llm

beforeToolCallback      -> execute_tool [getWeather]
afterToolCallback       -> execute_tool [getWeather]

beforeModelCallback #2 -> second call_llm
afterModelCallback  #2 -> second call_llm

Observed Behavior:

Without context propagation

beforeModelCallback M1 -> invoke_agent weather-agent
afterModelCallback  M1 -> first call_llm

beforeToolCallback  T1 -> execute_tool [getWeather]
afterToolCallback   T1 -> first call_llm

beforeModelCallback M2 -> execute_tool [getWeather]
afterModelCallback  M2 -> second call_llm

With context propgation

beforeModelCallback M1 -> invoke_agent weather-agent
afterModelCallback  M1 -> first call_llm

beforeToolCallback  T1 -> execute_tool [getWeather]
afterToolCallback   T1 -> execute_tool [getWeather] // fixes this

beforeModelCallback M2 -> previous call_llm or leaked downstream context
afterModelCallback  M2 -> second call_llm

Environment Details:

  • ADK Library Version (see maven dependency): 1.3.0
  • macOs

Model Information:

  • gemini-2.5-pro

🟡 Optional Information

How often has this issue occurred?:
Always (100%)

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions