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:
- Use ADK Java with OpenTelemetry tracing enabled, and with either manual context propagation or via a library.
- Add the following plugin that logs
Span.current() inside model and tool callbacks.
- Run a tool-calling agent with a prompt such as: “What is the weather in Sydney?”
- 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:
🟡 Optional Information
How often has this issue occurred?:
Always (100%)
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:Steps to Reproduce:
Span.current()inside model and tool callbacks.Span.current()span IDs to exported span IDs.Expected Behavior:
Plugin callbacks should observe the span corresponding to their semantic operation.
Observed Behavior:
Without context propagation
With context propgation
Environment Details:
Model Information:
🟡 Optional Information
How often has this issue occurred?:
Always (100%)