Skip to Content
DocsTracesGoogle ADK

Google ADK

Zero-config tracing for Google ADK  (Agent Development Kit) applications.

Google ADK already emits OpenTelemetry spans for agents, model calls, tools and workflows. Rhesis turns those spans into first-class traces with a single call: translate ADK’s spans into the Rhesis semantic conventions, pull prompts and completions out of ADK’s own attributes, and synthesize agent handoff spans for the Graph View. Your agent code does not change.

Installation

Install the SDK with the google-adk extra:

terminal
pip install "rhesis-sdk[google-adk]"

Quick Start

Create the RhesisClient first (it installs the OpenTelemetry tracer provider and Rhesis exporter), then call auto_instrument. After that, use ADK exactly as you normally would.

app.py
from rhesis.sdk import RhesisClient
from rhesis.sdk.telemetry import auto_instrument
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

# 1. Initialize Rhesis (sets up tracing). Required before auto_instrument().
client = RhesisClient(
    api_key="your-api-key",
    project_id="your-project-id",
    environment="development",
)

# 2. Enable Google ADK auto-instrumentation
auto_instrument("google_adk")  # alias: auto_instrument("adk")

# 3. Use ADK normally - every agent, model and tool call is traced
def check_order_status(order_id: str) -> dict:
    """Look up the shipping status of an order."""
    return {"order_id": order_id, "status": "in transit", "eta_days": 3}

agent = Agent(
    name="support_agent",
    model="gemini-3-flash",
    instruction="You are a helpful customer service assistant.",
    tools=[check_order_status],
)

session_service = InMemorySessionService()
await session_service.create_session(
    app_name="support", user_id="user-1", session_id="session-1"
)
runner = Runner(agent=agent, app_name="support", session_service=session_service)

async for event in runner.run_async(
    user_id="user-1",
    session_id="session-1",
    new_message=types.Content(
        role="user", parts=[types.Part(text="Where is order A-1234?")]
    ),
):
    print(event)

Order matters. auto_instrument() must run after RhesisClient is created. ADK emits spans as soon as any tracer provider exists, so with no Rhesis provider active there is nothing to translate: the call returns an empty list and logs a warning rather than letting untranslated spans reach the backend, where they would be rejected.

auto_instrument() with no arguments auto-detects every installed framework (including ADK). Pass "google_adk" (or "adk") to enable it explicitly. The call returns the list of frameworks that were instrumented:

app.py
enabled = auto_instrument("google_adk")
assert "google_adk" in enabled  # ["google_adk"]

What Gets Traced

Once enabled, ADK operations are captured automatically and mapped onto the Rhesis schema:

  • Agents - each agent activation becomes an ai.agent.invoke span carrying ai.agent.name
  • Model calls - each model call becomes one ai.llm.invoke span with model, provider, token counts (including cached and reasoning tokens), finish reason, and ai.prompt / ai.completion events carrying the real text
  • Tools - tool executions become ai.tool.invoke spans with ai.tool.input / ai.tool.output events
  • Handoffs - agent-to-agent delegation becomes ai.agent.handoff spans, for both ADK multi-agent mechanisms
  • Conversations - the run root carries the turn’s input and output, and every turn of a conversation lands in one trace rather than one trace per turn
  • Workflows and nodes - Workflow / node orchestration and the run root become function.google_adk.* spans

Operation Mapping

ADK spanRhesis span name
invoke_agent {name}ai.agent.invoke
call_llmai.llm.invoke
execute_tool {tool}ai.tool.invoke
execute_tool transfer_to_agentai.agent.handoff
invocation (schema v1 run root)function.google_adk.invocation
invoke_workflow {name}function.google_adk.workflow.{name}
invoke_node {name}function.google_adk.node.{name}
generate_content {model}dropped as a duplicate — see below

Spans that don’t match a known name fall back to function.google_adk.<name> so they always pass backend validation, with the original name preserved on the gen_ai.original_span_name attribute. ADK’s workflow spans cannot use an ai.* name because the Rhesis validator reserves workflow as a forbidden ai.* domain.

Two spans per model call

ADK wraps every model call in a call_llm span and then opens generate_content {model} inside it. Both describe the same call, so only one may become ai.llm.invoke — otherwise every model call would appear twice with double-counted tokens.

Rhesis keeps call_llm (the only one carrying prompts and completions with default settings, and the one with the fuller token breakdown) and drops the inner duplicate. Because ADK parents the execute_tool spans on that inner span, the tool spans are re-pointed at call_llm so nothing orphans:

Multi-Agent Handoffs

ADK has two ways for one agent to hand work to another, and they produce structurally different traces. Rhesis draws ai.agent.handoff edges for both, so the Graph View is connected either way.

sub_agents + transfer_to_agent — the model calls ADK’s built-in transfer_to_agent tool and control moves across. The target agent’s own span is a sibling of the caller’s, so the transfer tool span is the only place the edge is observable; Rhesis translates that span itself into ai.agent.handoff rather than adding a tool row for it.

AgentTool — a whole agent registered as a tool. Rhesis keeps the ai.tool.invoke span (an AgentTool call genuinely is a tool call) and synthesizes an extra ai.agent.handoff span alongside it.

Each handoff span carries ai.agent.handoff.from and ai.agent.handoff.to. See the Multi-Agent Tracing page for the full span reference.

The deprecated SequentialAgent / LoopAgent / ParallelAgent classes also produce handoff edges, from the orchestrator to each step, because their step spans nest under the orchestrator’s own agent span. The newer Workflow API nests its steps under invoke_workflow instead, so its structure shows as span nesting rather than as edges.

Conversation Grouping

ADK opens its own root span for every turn, and OpenTelemetry gives each one a fresh trace id. Rhesis instead shows a conversation as a single trace whose roots are the individual turns. To get that, set a conversation id around each turn — the integration both stamps the run root as a conversation turn root and puts every turn on one trace:

session.py
from rhesis.telemetry.context import get_conversation_id, set_conversation_id

previous = get_conversation_id()
set_conversation_id(conversation_id)
try:
    async for event in runner.run_async(...):
        ...
finally:
    set_conversation_id(previous)

That is the whole integration: no per-turn span to open, nothing to thread through your own loop. The conversation’s trace is the first turn’s own trace; later turns are moved onto it. The first turn is never moved, so any trace id already recorded for it still resolves.

This applies only to a standalone ADK run — a terminal chat, a script, anything where ADK’s own run span is the top of the trace and no part of Rhesis has published an id for it. Behind @endpoint or @observe, or anywhere the platform supplied a conversation trace id, Rhesis owns the trace and already joins the turns itself, so the integration leaves the ids untouched. That id is what the platform returns as the endpoint result, persists as the conversation’s trace id for the next turn, and writes its own turn record to — including the reply. Moving spans off it would separate the agent’s spans from their own reply.

Turn labelling and turn joining need different things. With no conversation id set, the turn root is still labelled with the ADK session id, so an app that reuses one session across turns is grouped in the Conversation tab. It is not joined into one trace, though: the target trace has to be known when ADK creates the run span, and ADK assigns no span attributes at that point, so only the conversation id from rhesis.telemetry.context is readable that early. Set it explicitly if you want one trace per conversation.

When the Reply Is Not the Model’s Last Message

The turn’s recorded reply is extracted from ADK’s model spans, which is right when your agent answers with model text. It cannot work when the reply comes from somewhere else — a terminal tool’s output, a template, a branch in your own code. None of that reaches an llm_response attribute, and ADK’s run span has already ended by the time your code holds the reply, so those turns show an empty reply in the Conversation tab.

If your agent answers that way, own the turn instead — see Owning the Turn:

session.py
from rhesis.telemetry import conversation_turn

with conversation_turn(conversation_id, input=message) as turn:
    result = await run_turn_async(message)
    turn.output = result["response"]

The ADK run then nests under that span, which becomes the turn root and carries the reply your user actually saw. This is what agents/reg-advisor/ does, because its reply can come from a terminal tool or a refusal composed in Python.

A step that ADK blocks on its own max_llm_calls budget still opens a call_llm span, so it still becomes an ai.llm.invoke — with no prompt or completion and near-zero duration. Blocked steps therefore count towards the model calls you see on a trace.

Telemetry Schema Versions

ADK supports two telemetry formats, selected with ADK_TELEMETRY_SCHEMA_VERSION_OPT_IN (1 or 2; the default is 2 on Vertex Agent Engine and 1 elsewhere). Both are supported, and Rhesis does not force either — the only difference is which span is the run root (invocation in v1, invoke_workflow {entrypoint} in v2) and the integration decides that structurally rather than by name.

Content Capture and Privacy

By default, the integration captures message content (prompts, completions, tool arguments and results) so traces show what each agent actually sent and received. It reads this from ADK’s own span attributes, which record it out of the box.

To omit message content — for example in production or regulated environments — set RHESIS_DISABLE_CONTENT_CAPTURE before initializing:

terminal
export RHESIS_DISABLE_CONTENT_CAPTURE=1

Accepted truthy values: 1, true, yes, on (case-insensitive). When disabled, spans are still exported with structural metadata (model, provider, token counts, agent names, operation types), but no prompt, completion, or tool input/output payloads are recorded.

The integration switches off all three of ADK’s overlapping content knobs, including ADK_TELEMETRY_IGNORE_RUN_CONFIG — without that one a per-request RunConfig.telemetry would outrank the environment and put prompts back on the spans. It never touches OTEL_SEMCONV_STABILITY_OPT_IN, which is a global OpenTelemetry switch that would change the behaviour of unrelated instrumentation in the same process.

Span Verbosity

To keep traces readable, the integration drops ADK infrastructure spans that carry no agent, model or tool payload: send_data, create_cache, handle_context_caching, compact_events, and execute_tool (merged) (a duplicate summary ADK emits after parallel tool calls, whose payload is placeholder text).

To keep every span, set:

terminal
export RHESIS_GOOGLE_ADK_VERBOSE_SPANS=1

They then appear under function.google_adk.*, never claiming an ai.* semantic.

Combining with Decorators

Auto-instrumentation composes with @endpoint and @observe. Wrap your entry point with @endpoint so Rhesis can run tests against it; ADK spans created inside are nested under the endpoint span, which then owns the turn-root semantics:

app.py
from rhesis.sdk import RhesisClient, endpoint
from rhesis.sdk.telemetry import auto_instrument

client = RhesisClient(api_key="your-api-key", project_id="your-project-id")
auto_instrument("google_adk")

@endpoint()
async def chat(input: str, conversation_id: str = None) -> dict:
    # ADK agent/model/tool/handoff spans are nested under this endpoint span
    reply = await run_turn(input, conversation_id=conversation_id)
    return {"output": reply, "conversation_id": conversation_id}

Use Runner.run_async, not Runner.run. The synchronous run executes the agent on a fresh thread with fresh context variables, so its spans start a new trace instead of nesting under the enclosing endpoint span, and the conversation id set via rhesis.telemetry.context is invisible to them.

Do not call google.adk.telemetry.setup.maybe_set_otel_providers(). It installs its own tracer, logger and meter providers, replacing the ones Rhesis configured, and spans will no longer reach Rhesis.

Troubleshooting

auto_instrument("google_adk") returns an empty list. Either google-adk is not installed, or no Rhesis tracer provider is active. Create RhesisClient before calling auto_instrument, and check the log — the integration says which of the two it was.

Spans are missing from the trace. ADK spans nest under whichever span is active when the run starts. If you are using the synchronous Runner.run, switch to run_async (see the warning above).

No prompts or completions. Check whether RHESIS_DISABLE_CONTENT_CAPTURE is set, or whether the app sets ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS=false itself.

google-adk[otel-gcp] is installed. That extra brings opentelemetry-instrumentation-google-genai, which makes ADK hand the model span to a different instrumentation scope. Traces stay complete (call_llm is still ADK’s own and still becomes ai.llm.invoke), but that library’s spans are not translated. The extra is not needed for Rhesis tracing.

Worked Example: Reg-Advisor

The repository ships a runnable multi-agent ADK demo — a regulatory coordinator that delegates to three specialists via AgentTool — wired to produce agent, LLM, tool and handoff traces in Rhesis. See agents/reg-advisor and its architecture notes .

Each of its entry points has a traced twin that wraps the plain one, which is a pattern worth copying: the untraced path stays the one under test, and turning tracing on never means editing the code that does the work.

terminal
uv run python chat_terminal/chat.py            # interactive REPL
uv run python chat_terminal/chat_traced.py     # the same REPL, traced

uv run python examples/run_scenarios.py        # scripted scenarios
uv run python examples/run_scenarios_traced.py # the same scenarios, traced

Testing ADK Agents

Beyond tracing, Rhesis can run autonomous multi-turn conversation tests against an ADK agent with the GoogleADKTarget — see Conversation Simulation.

Disabling

Turn instrumentation off (restores the original exporter and any environment variable the integration set) with:

app.py
from rhesis.sdk.telemetry import disable_auto_instrument

disable_auto_instrument()

ADK itself has no instrumentation switch, so it keeps emitting spans after disable(). Those spans then pass through untranslated — the same state as never having enabled the integration.


Related: