Skip to content

Commit 84dff10

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: Fixing tracing for function calls
Fixing when the execute tool happens in the graph. Refactor and simplify the tracing logic for function and tool calls within the ADK. The primary goal is to consolidate multiple tracing events into more cohesive operations, specifically merging "tool_call" and "tool_response" into a single "execute_tool" operation. ### Key Changes: * **Consolidated Tool Tracing:** Replaced the separate `traceToolCall` and `traceToolResponse` methods with a unified `traceToolExecution` in `Tracing.java`. This reduces span noise by representing a tool's lifecycle as a single "execute_tool" operation containing both arguments and results (or errors). * **Standardized Operation Names:** Introduced constants for core Gen AI operations: `invoke_agent`, `execute_tool`, `send_data`, and `call_llm`. * **Improved Error Tracing:** `traceToolExecution` and `traceCallLlm` now explicitly accept an optional `Exception`, allowing them to automatically set the span status to error and record the exception. * **Refactored Tracing API:** * `traceSendData` and other methods now require an explicit `Span` argument, moving away from implicit context lookups where appropriate. * Added `traceMergedToolCalls` to specifically handle the telemetry for parallel tool executions. * **Flow Logic Cleanup:** Simplified `Functions.java` and `BaseLlmFlow.java` by removing redundant context passing and aligning with the new consolidated tracing methods. * **Test Suite Updates:** Significantly updated `ContextPropagationTest.java` to reflect the new tracing model. Several manual hierarchy tests were removed in favor of testing the consolidated `execute_tool` logic and updated attributes. PiperOrigin-RevId: 889246953
1 parent 3650c7f commit 84dff10

6 files changed

Lines changed: 208 additions & 378 deletions

File tree

core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ private Flowable<Event> callLlm(
233233
callLlmContext)
234234
.doOnSubscribe(
235235
s ->
236-
Tracing.traceCallLlm(
236+
traceCallLlm(
237237
span,
238238
context,
239239
eventForCallbackUsage.id(),
@@ -520,6 +520,7 @@ public Flowable<Event> runLive(InvocationContext invocationContext) {
520520
.doOnComplete(
521521
() ->
522522
Tracing.traceSendData(
523+
Span.current(),
523524
invocationContext,
524525
eventIdForSendData,
525526
llmRequestAfterPreprocess.contents()))
@@ -529,6 +530,7 @@ public Flowable<Event> runLive(InvocationContext invocationContext) {
529530
span.setStatus(StatusCode.ERROR, error.getMessage());
530531
span.recordException(error);
531532
Tracing.traceSendData(
533+
Span.current(),
532534
invocationContext,
533535
eventIdForSendData,
534536
llmRequestAfterPreprocess.contents());
@@ -706,6 +708,19 @@ private Flowable<Event> buildPostprocessingEvents(
706708
return processorEvents.concatWith(Flowable.just(modelResponseEvent)).concatWith(functionEvents);
707709
}
708710

711+
/**
712+
* Traces an LLM call without an associated exception. This is an overload for {@link
713+
* Tracing#traceCallLlm} for successful calls.
714+
*/
715+
private void traceCallLlm(
716+
Span span,
717+
InvocationContext context,
718+
String eventId,
719+
LlmRequest llmRequest,
720+
LlmResponse llmResponse) {
721+
Tracing.traceCallLlm(span, context, eventId, llmRequest, llmResponse, null);
722+
}
723+
709724
private Event buildModelResponseEvent(
710725
Event baseEventForLlmResponse, LlmRequest llmRequest, LlmResponse llmResponse) {
711726
Event.Builder eventBuilder =

core/src/main/java/com/google/adk/flows/llmflows/Functions.java

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,12 @@ public static Maybe<Event> handleFunctionCalls(
178178

179179
if (events.size() > 1) {
180180
return Maybe.just(mergedEvent)
181-
.doOnSuccess(event -> Tracing.traceToolResponse(event.id(), event))
182-
.compose(Tracing.<Event>trace("tool_response").setParent(parentContext));
181+
.compose(
182+
Tracing.<Event>trace("execute_tool (merged)")
183+
.setParent(parentContext)
184+
.onSuccess(
185+
(span, event) ->
186+
Tracing.traceMergedToolCalls(span, event.id(), event)));
183187
}
184188
return Maybe.just(mergedEvent);
185189
});
@@ -269,10 +273,8 @@ private static Function<FunctionCall, Maybe<Event>> getFunctionCallMapper(
269273
tool,
270274
toolContext,
271275
functionCall,
272-
functionArgs,
273-
parentContext)
274-
: callTool(
275-
tool, functionArgs, toolContext, parentContext))
276+
functionArgs)
277+
: callTool(tool, functionArgs, toolContext))
276278
.compose(Tracing.withContext(parentContext)));
277279

278280
return postProcessFunctionResult(
@@ -296,8 +298,7 @@ private static Maybe<Map<String, Object>> processFunctionLive(
296298
BaseTool tool,
297299
ToolContext toolContext,
298300
FunctionCall functionCall,
299-
Map<String, Object> args,
300-
Context parentContext) {
301+
Map<String, Object> args) {
301302
// Case 1: Handle a call to stopStreaming
302303
if (functionCall.name().get().equals("stopStreaming") && args.containsKey("functionName")) {
303304
String functionNameToStop = (String) args.get("functionName");
@@ -365,7 +366,7 @@ private static Maybe<Map<String, Object>> processFunctionLive(
365366
}
366367

367368
// Case 3: Fallback for regular, non-streaming tools
368-
return callTool(tool, args, toolContext, parentContext);
369+
return callTool(tool, args, toolContext);
369370
}
370371

371372
public static Set<String> getLongRunningFunctionCalls(
@@ -426,12 +427,22 @@ private static Maybe<Event> postProcessFunctionResult(
426427
Event event =
427428
buildResponseEvent(
428429
tool, finalFunctionResult, toolContext, invocationContext);
429-
Tracing.traceToolResponse(event.id(), event);
430430
return Maybe.just(event);
431431
});
432432
})
433433
.compose(
434-
Tracing.<Event>trace("tool_response [" + tool.name() + "]").setParent(parentContext));
434+
Tracing.<Event>trace("execute_tool [" + tool.name() + "]")
435+
.setParent(parentContext)
436+
.onSuccess(
437+
(span, event) ->
438+
Tracing.traceToolExecution(
439+
span,
440+
tool.name(),
441+
tool.description(),
442+
tool.getClass().getSimpleName(),
443+
functionArgs,
444+
event,
445+
null)));
435446
}
436447

437448
private static Optional<Event> mergeParallelFunctionResponseEvents(
@@ -579,17 +590,10 @@ private static Maybe<Map<String, Object>> maybeInvokeAfterToolCall(
579590
}
580591

581592
private static Maybe<Map<String, Object>> callTool(
582-
BaseTool tool, Map<String, Object> args, ToolContext toolContext, Context parentContext) {
593+
BaseTool tool, Map<String, Object> args, ToolContext toolContext) {
583594
return tool.runAsync(args, toolContext)
584595
.toMaybe()
585-
.doOnSubscribe(
586-
d ->
587-
Tracing.traceToolCall(
588-
tool.name(), tool.description(), tool.getClass().getSimpleName(), args))
589596
.doOnError(t -> Span.current().recordException(t))
590-
.compose(
591-
Tracing.<Map<String, Object>>trace("tool_call [" + tool.name() + "]")
592-
.setParent(parentContext))
593597
.onErrorResumeNext(
594598
e ->
595599
Maybe.error(

core/src/main/java/com/google/adk/telemetry/Tracing.java

Lines changed: 107 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.opentelemetry.api.GlobalOpenTelemetry;
3434
import io.opentelemetry.api.common.AttributeKey;
3535
import io.opentelemetry.api.trace.Span;
36+
import io.opentelemetry.api.trace.StatusCode;
3637
import io.opentelemetry.api.trace.Tracer;
3738
import io.opentelemetry.context.Context;
3839
import io.opentelemetry.context.Scope;
@@ -61,6 +62,7 @@
6162
import java.util.function.BiConsumer;
6263
import java.util.function.Consumer;
6364
import java.util.function.Supplier;
65+
import org.jspecify.annotations.Nullable;
6466
import org.reactivestreams.Publisher;
6567
import org.reactivestreams.Subscriber;
6668
import org.reactivestreams.Subscription;
@@ -77,6 +79,11 @@ public class Tracing {
7779

7880
private static final Logger log = LoggerFactory.getLogger(Tracing.class);
7981

82+
private static final String INVOKE_AGENT_OPERATION = "invoke_agent";
83+
private static final String EXECUTE_TOOL_OPERATION = "execute_tool";
84+
private static final String SEND_DATA_OPERATION = "send_data";
85+
private static final String CALL_LLM_OPERATION = "call_llm";
86+
8087
private static final AttributeKey<List<String>> GEN_AI_RESPONSE_FINISH_REASONS =
8188
AttributeKey.stringArrayKey("gen_ai.response.finish_reasons");
8289

@@ -134,15 +141,6 @@ public class Tracing {
134141

135142
private Tracing() {}
136143

137-
private static void traceWithSpan(String methodName, Consumer<Span> traceAction) {
138-
Span span = Span.current();
139-
if (!span.getSpanContext().isValid()) {
140-
log.trace("{}: No valid span in current context.", methodName);
141-
return;
142-
}
143-
traceAction.accept(span);
144-
}
145-
146144
private static void setInvocationAttributes(
147145
Span span, InvocationContext invocationContext, String eventId) {
148146
span.setAttribute(ADK_INVOCATION_ID, invocationContext.invocationId());
@@ -159,12 +157,6 @@ private static void setInvocationAttributes(
159157
}
160158
}
161159

162-
private static void setToolExecutionAttributes(Span span) {
163-
span.setAttribute(GEN_AI_OPERATION_NAME, "execute_tool");
164-
span.setAttribute(ADK_LLM_REQUEST, "{}");
165-
span.setAttribute(ADK_LLM_RESPONSE, "{}");
166-
}
167-
168160
private static void setJsonAttribute(Span span, AttributeKey<String> key, Object value) {
169161
if (!CAPTURE_MESSAGE_CONTENT_IN_SPANS) {
170162
span.setAttribute(key, "{}");
@@ -198,7 +190,7 @@ public static void setTracerForTesting(Tracer tracer) {
198190
*/
199191
public static void traceAgentInvocation(
200192
Span span, String agentName, String agentDescription, InvocationContext invocationContext) {
201-
span.setAttribute(GEN_AI_OPERATION_NAME, "invoke_agent");
193+
span.setAttribute(GEN_AI_OPERATION_NAME, INVOKE_AGENT_OPERATION);
202194
span.setAttribute(GEN_AI_AGENT_DESCRIPTION, agentDescription);
203195
span.setAttribute(GEN_AI_AGENT_NAME, agentName);
204196
if (invocationContext.session() != null && invocationContext.session().id() != null) {
@@ -207,58 +199,62 @@ public static void traceAgentInvocation(
207199
}
208200

209201
/**
210-
* Traces tool call arguments.
211-
*
212-
* @param args The arguments to the tool call.
213-
*/
214-
public static void traceToolCall(
215-
String toolName, String toolDescription, String toolType, Map<String, Object> args) {
216-
traceWithSpan(
217-
"traceToolCall",
218-
span -> {
219-
setToolExecutionAttributes(span);
220-
span.setAttribute(GEN_AI_TOOL_NAME, toolName);
221-
span.setAttribute(GEN_AI_TOOL_DESCRIPTION, toolDescription);
222-
span.setAttribute(GEN_AI_TOOL_TYPE, toolType);
223-
224-
setJsonAttribute(span, ADK_TOOL_CALL_ARGS, args);
225-
});
226-
}
227-
228-
/**
229-
* Traces tool response event.
202+
* Traces a tool execution, including its arguments, response, and any potential error.
230203
*
231-
* @param eventId The ID of the event.
232-
* @param functionResponseEvent The function response event.
204+
* @param span The span representing the tool execution.
205+
* @param toolName The name of the tool.
206+
* @param toolDescription The tool's description.
207+
* @param toolType The tool's type (e.g., "FunctionTool").
208+
* @param args The arguments passed to the tool.
209+
* @param functionResponseEvent The event containing the tool's response, if successful.
210+
* @param error The exception thrown during execution, if any.
233211
*/
234-
public static void traceToolResponse(String eventId, Event functionResponseEvent) {
235-
traceWithSpan(
236-
"traceToolResponse",
237-
span -> {
238-
setToolExecutionAttributes(span);
239-
span.setAttribute(ADK_EVENT_ID, eventId);
240-
241-
FunctionResponse functionResponse =
242-
functionResponseEvent.functionResponses().stream().findFirst().orElse(null);
243-
244-
String toolCallId = "<not specified>";
245-
Object toolResponse = "<not specified>";
246-
if (functionResponse != null) {
247-
toolCallId = functionResponse.id().orElse(toolCallId);
248-
if (functionResponse.response().isPresent()) {
249-
toolResponse = functionResponse.response().get();
250-
}
251-
}
252-
253-
span.setAttribute(GEN_AI_TOOL_CALL_ID, toolCallId);
212+
public static void traceToolExecution(
213+
Span span,
214+
String toolName,
215+
String toolDescription,
216+
String toolType,
217+
Map<String, Object> args,
218+
@Nullable Event functionResponseEvent,
219+
@Nullable Exception error) {
220+
span.setAttribute(GEN_AI_OPERATION_NAME, EXECUTE_TOOL_OPERATION);
221+
span.setAttribute(GEN_AI_TOOL_NAME, toolName);
222+
span.setAttribute(GEN_AI_TOOL_DESCRIPTION, toolDescription);
223+
span.setAttribute(GEN_AI_TOOL_TYPE, toolType);
224+
225+
setJsonAttribute(span, ADK_TOOL_CALL_ARGS, args);
226+
227+
if (functionResponseEvent != null) {
228+
span.setAttribute(ADK_EVENT_ID, functionResponseEvent.id());
229+
FunctionResponse functionResponse =
230+
functionResponseEvent.functionResponses().stream().findFirst().orElse(null);
231+
232+
String toolCallId = "<not specified>";
233+
Object toolResponse = "<not specified>";
234+
if (functionResponse != null) {
235+
toolCallId = functionResponse.id().orElse(toolCallId);
236+
if (functionResponse.response().isPresent()) {
237+
toolResponse = functionResponse.response().get();
238+
}
239+
}
240+
span.setAttribute(GEN_AI_TOOL_CALL_ID, toolCallId);
241+
Object finalToolResponse =
242+
(toolResponse instanceof Map) ? toolResponse : ImmutableMap.of("result", toolResponse);
243+
setJsonAttribute(span, ADK_TOOL_RESPONSE, finalToolResponse);
244+
} else {
245+
// Set placeholder if no response event is available (e.g., due to an error)
246+
span.setAttribute(GEN_AI_TOOL_CALL_ID, "<not specified>");
247+
setJsonAttribute(span, ADK_TOOL_RESPONSE, "{}");
248+
}
254249

255-
Object finalToolResponse =
256-
(toolResponse instanceof Map)
257-
? toolResponse
258-
: ImmutableMap.of("result", toolResponse);
250+
// Also set empty LLM attributes for UI compatibility, like in traceToolResponse
251+
span.setAttribute(ADK_LLM_REQUEST, "{}");
252+
span.setAttribute(ADK_LLM_RESPONSE, "{}");
259253

260-
setJsonAttribute(span, ADK_TOOL_RESPONSE, finalToolResponse);
261-
});
254+
if (error != null) {
255+
span.setStatus(StatusCode.ERROR, error.getMessage());
256+
span.recordException(error);
257+
}
262258
}
263259

264260
/**
@@ -303,15 +299,22 @@ public static void traceCallLlm(
303299
InvocationContext invocationContext,
304300
String eventId,
305301
LlmRequest llmRequest,
306-
LlmResponse llmResponse) {
302+
LlmResponse llmResponse,
303+
@Nullable Exception error) {
307304
span.setAttribute(GEN_AI_SYSTEM, "gcp.vertex.agent");
305+
span.setAttribute(GEN_AI_OPERATION_NAME, CALL_LLM_OPERATION);
308306
llmRequest.model().ifPresent(modelName -> span.setAttribute(GEN_AI_REQUEST_MODEL, modelName));
309307

310308
setInvocationAttributes(span, invocationContext, eventId);
311309

312310
setJsonAttribute(span, ADK_LLM_REQUEST, buildLlmRequestForTrace(llmRequest));
313311
setJsonAttribute(span, ADK_LLM_RESPONSE, llmResponse);
314312

313+
if (error != null) {
314+
span.setStatus(StatusCode.ERROR, error.getMessage());
315+
span.recordException(error);
316+
}
317+
315318
llmRequest
316319
.config()
317320
.ifPresent(
@@ -352,18 +355,45 @@ public static void traceCallLlm(
352355
* @param data A list of content objects being sent.
353356
*/
354357
public static void traceSendData(
355-
InvocationContext invocationContext, String eventId, List<Content> data) {
356-
traceWithSpan(
357-
"traceSendData",
358-
span -> {
359-
setInvocationAttributes(span, invocationContext, eventId);
360-
361-
ImmutableList<Content> safeData =
362-
Optional.ofNullable(data).orElse(ImmutableList.of()).stream()
363-
.filter(Objects::nonNull)
364-
.collect(toImmutableList());
365-
setJsonAttribute(span, ADK_DATA, safeData);
366-
});
358+
Span span, InvocationContext invocationContext, String eventId, List<Content> data) {
359+
if (!span.getSpanContext().isValid()) {
360+
log.trace("traceSendData: No valid span in current context.");
361+
return;
362+
}
363+
setInvocationAttributes(span, invocationContext, eventId);
364+
span.setAttribute(GEN_AI_OPERATION_NAME, SEND_DATA_OPERATION);
365+
366+
ImmutableList<Content> safeData =
367+
Optional.ofNullable(data).orElse(ImmutableList.of()).stream()
368+
.filter(Objects::nonNull)
369+
.collect(toImmutableList());
370+
setJsonAttribute(span, ADK_DATA, safeData);
371+
}
372+
373+
/**
374+
* Traces merged tool call events.
375+
*
376+
* <p>Calling this function is not needed for telemetry purposes. This is provided for preventing
377+
* /debug/trace requests (typically sent by web UI).
378+
*
379+
* @param responseEventId The ID of the response event.
380+
* @param functionResponseEvent The merged response event.
381+
*/
382+
public static void traceMergedToolCalls(
383+
Span span, String responseEventId, Event functionResponseEvent) {
384+
if (!span.getSpanContext().isValid()) {
385+
log.trace("traceMergedToolCalls: No valid span in current context.");
386+
return;
387+
}
388+
span.setAttribute(GEN_AI_OPERATION_NAME, EXECUTE_TOOL_OPERATION);
389+
span.setAttribute(GEN_AI_TOOL_NAME, "(merged tools)");
390+
span.setAttribute(GEN_AI_TOOL_DESCRIPTION, "(merged tools)");
391+
span.setAttribute(GEN_AI_TOOL_CALL_ID, responseEventId);
392+
span.setAttribute(ADK_TOOL_CALL_ARGS, "N/A");
393+
span.setAttribute(ADK_EVENT_ID, responseEventId);
394+
setJsonAttribute(span, ADK_TOOL_RESPONSE, functionResponseEvent);
395+
span.setAttribute(ADK_LLM_REQUEST, "{}");
396+
span.setAttribute(ADK_LLM_RESPONSE, "{}");
367397
}
368398

369399
/**

0 commit comments

Comments
 (0)