Skip to content

Conversation

@mportdata
Copy link

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
  • Partially closes: #2792

Problem:
Telemetry spans are always emitted. There wasn’t a
consistent, ADK‑level switch to disable all
telemetry.

Solution:
I've added a disable_telemetry attribute to the BaseAgent class and 2 environment variables OTEL_SDK_DISABLED and ADK_TELEMETRY_DISABLED. When any of the above are set to true then spans are no longer started resulting in tracing being disabled.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my
    change.
  • All unit tests pass locally.

Integration Tests:

I have created unit tests in the following file /tests/unittests/telemetry/test_telemetry_disable_agent.py
These tests monkey patch the start_as_current_span method used for tracing throughout Google ADK and ensures it is only called when tracing is enabled both via the agent attribute or the environment variables. Conversely I have also tested here that telemetry is returned when expected (when not disabled by either the attribute or the environment variables).

Summary:

  • uv run pytest tests/unittests
  • I have added unit tests in /tests/unittests/telemetry/test_telemetry_disable_agent.py - these unit tests check that zero spans are emitted when telemetry is disabled either by the agent attribute or the environment variables, and that spans are emitted when not set by either.

Manual End-to-End (E2E) Tests:

A simple ADK agent has been tested by running uv run adk web with the disable_telemetry=True behaved as expected.

from google.adk.agents import Agent

root_agent = Agent(
    name="search_assistant",
    model="gemini-2.0-flash-lite",
    instruction="You are a helpful assistant.",
    description="You are a friendly agent",
    disable_telemetry=True,
)
image

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own
    code.
  • I have commented my code, particularly in
    hard-to-understand areas.
  • I have added tests that prove my fix is
    effective or that my feature works.
  • New and existing unit tests pass locally
    with my changes.
  • I have manually tested my changes end-to-
    end.
  • Any dependent changes have been merged and
    published in downstream modules.

@google-cla
Copy link

google-cla bot commented Dec 27, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @mportdata, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request provides a robust and flexible way to disable telemetry across the Agent Development Kit (ADK). Previously, telemetry spans were always emitted, but now users can control this behavior through a new disable_telemetry attribute on the BaseAgent class or by setting specific environment variables. This enhancement improves user control over data emission and integrates seamlessly into the existing agent execution flow, ensuring that tracing is only active when desired.

Highlights

  • Telemetry Disabling Mechanism: Introduced a new disable_telemetry attribute to the BaseAgent class, allowing users to disable telemetry directly on an agent instance.
  • Environment Variable Control: Added support for two environment variables, OTEL_SDK_DISABLED and ADK_TELEMETRY_DISABLED, which can be set to 'true' or '1' to globally disable telemetry spans.
  • Centralized Telemetry Utility: A new utility module, telemetry_utils.py, was added to centralize the logic for checking whether telemetry is enabled, simplifying checks across the codebase.
  • Refactored Agent Execution Flow: The BaseAgent's run_async and run_live methods were refactored to use a new _run_callbacks_and_impl method, which conditionally starts telemetry spans based on the new disabling mechanisms.
  • Comprehensive Testing: New unit and integration tests have been added to thoroughly verify that telemetry is correctly disabled when the attribute or environment variables are set.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the tracing [Component] This issue is related to OpenTelemetry tracing label Dec 27, 2025
@adk-bot
Copy link
Collaborator

adk-bot commented Dec 27, 2025

Response from ADK Triaging Agent

Hello @mportdata, thank you for your contribution!

Before we can review your pull request, you'll need to sign our Contributor License Agreement (CLA). It seems that the CLA check has failed.

You can review and sign the CLA here: https://cla.developers.google.com/

Once the CLA is signed, we can proceed with the review. Thanks!

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new disable_telemetry flag to BaseAgent and Gemini LLM classes, allowing users to explicitly control OpenTelemetry tracing for agents and LLM interactions. The changes involve adding the is_telemetry_enabled utility function and refactoring existing code to conditionally apply tracing spans based on this flag. Specifically, run_async and run_live methods in base_agent.py, _call_llm_async in base_llm_flow.py, and tool execution in functions.py now check this flag before creating spans. The GeminiContextCacheManager and Gemini LLM also incorporate this flag to conditionally trace cache creation and LLM calls. Additionally, many string literals across various files were updated from single quotes to double quotes for consistency, and minor formatting adjustments were made to import statements. Review comments highlighted opportunities to further reduce code duplication by using contextlib.nullcontext for conditional tracing and corrected a typo in a docstring example.

Comment on lines 365 to 377
if not self.disable_telemetry:
from ..telemetry.tracing import tracer

with tracer.start_as_current_span("create_cache") as span:
return await self._create_gemini_cache_body(
llm_request=llm_request,
cache_contents_count=cache_contents_count,
span=span,
)
else:
return await self._create_gemini_cache_body(
llm_request=llm_request, cache_contents_count=cache_contents_count
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's some code duplication here. You can use contextlib.nullcontext to conditionally apply the tracing span, which will make the code cleaner and more maintainable.

You'll need to add import contextlib at the top of the file.

    span_context = contextlib.nullcontext()
    if not self.disable_telemetry:
      from ..telemetry.tracing import tracer
      span_context = tracer.start_as_current_span("create_cache")

    with span_context as span:
      return await self._create_gemini_cache_body(
          llm_request=llm_request,
          cache_contents_count=cache_contents_count,
          span=span,
      )

Comment on lines 171 to 195
if llm_request.cache_config:
from ..telemetry.tracing import tracer
from .gemini_context_cache_manager import GeminiContextCacheManager

with tracer.start_as_current_span('handle_context_caching') as span:
cache_manager = GeminiContextCacheManager(self.api_client)
if not self.disable_telemetry:
from ..telemetry.tracing import tracer

with tracer.start_as_current_span("handle_context_caching") as span:
cache_manager = GeminiContextCacheManager(
self.api_client, disable_telemetry=self.disable_telemetry
)
cache_metadata = await cache_manager.handle_context_caching(
llm_request
)
if cache_metadata:
if cache_metadata.cache_name:
span.set_attribute("cache_action", "active_cache")
span.set_attribute("cache_name", cache_metadata.cache_name)
else:
span.set_attribute("cache_action", "fingerprint_only")
else:
cache_manager = GeminiContextCacheManager(
self.api_client, disable_telemetry=self.disable_telemetry
)
cache_metadata = await cache_manager.handle_context_caching(llm_request)
if cache_metadata:
if cache_metadata.cache_name:
span.set_attribute('cache_action', 'active_cache')
span.set_attribute('cache_name', cache_metadata.cache_name)
else:
span.set_attribute('cache_action', 'fingerprint_only')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's some code duplication in this block. You can refactor it to avoid repeating the logic for creating GeminiContextCacheManager and calling handle_context_caching. Using contextlib.nullcontext can help conditionally apply the tracing span.

You'll need to add import contextlib at the top of the file.

Suggested change
if llm_request.cache_config:
from ..telemetry.tracing import tracer
from .gemini_context_cache_manager import GeminiContextCacheManager
with tracer.start_as_current_span('handle_context_caching') as span:
cache_manager = GeminiContextCacheManager(self.api_client)
if not self.disable_telemetry:
from ..telemetry.tracing import tracer
with tracer.start_as_current_span("handle_context_caching") as span:
cache_manager = GeminiContextCacheManager(
self.api_client, disable_telemetry=self.disable_telemetry
)
cache_metadata = await cache_manager.handle_context_caching(
llm_request
)
if cache_metadata:
if cache_metadata.cache_name:
span.set_attribute("cache_action", "active_cache")
span.set_attribute("cache_name", cache_metadata.cache_name)
else:
span.set_attribute("cache_action", "fingerprint_only")
else:
cache_manager = GeminiContextCacheManager(
self.api_client, disable_telemetry=self.disable_telemetry
)
cache_metadata = await cache_manager.handle_context_caching(llm_request)
if cache_metadata:
if cache_metadata.cache_name:
span.set_attribute('cache_action', 'active_cache')
span.set_attribute('cache_name', cache_metadata.cache_name)
else:
span.set_attribute('cache_action', 'fingerprint_only')
if llm_request.cache_config:
from .gemini_context_cache_manager import GeminiContextCacheManager
cache_manager = GeminiContextCacheManager(
self.api_client, disable_telemetry=self.disable_telemetry
)
span_context = contextlib.nullcontext()
if not self.disable_telemetry:
from ..telemetry.tracing import tracer
span_context = tracer.start_as_current_span("handle_context_caching")
with span_context as span:
cache_metadata = await cache_manager.handle_context_caching(llm_request)
if span and cache_metadata:
if cache_metadata.cache_name:
span.set_attribute("cache_action", "active_cache")
span.set_attribute("cache_name", cache_metadata.cache_name)
else:
span.set_attribute("cache_action", "fingerprint_only")

Comment on lines 503 to 520
async def _run_with_optional_trace(
agent: BaseAgent,
new_message: Optional[types.Content] = None,
invocation_id: Optional[str] = None,
) -> AsyncGenerator[Event, None]:
if is_telemetry_enabled(agent):
with tracer.start_as_current_span("invocation"):
async with Aclosing(
_run_body(new_message=new_message, invocation_id=invocation_id)
) as agen:
async for e in agen:
yield e
else:
async with Aclosing(
self._exec_with_plugin(
invocation_context=invocation_context,
session=session,
execute_fn=execute,
is_live_call=False,
)
_run_body(new_message=new_message, invocation_id=invocation_id)
) as agen:
async for event in agen:
yield event
# Run compaction after all events are yielded from the agent.
# (We don't compact in the middle of an invocation, we only compact at
# the end of an invocation.)
if self.app and self.app.events_compaction_config:
logger.debug('Running event compactor.')
await _run_compaction_for_sliding_window(
self.app, session, self.session_service
)
async for e in agen:
yield e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The async with Aclosing(...) block is duplicated. You can refactor this using contextlib.nullcontext to conditionally apply the tracing span, which will make the code more DRY (Don't Repeat Yourself).

You'll need to add import contextlib at the top of the file.

    async def _run_with_optional_trace(
        agent: BaseAgent,
        new_message: Optional[types.Content] = None,
        invocation_id: Optional[str] = None,
    ) -> AsyncGenerator[Event, None]:
      span_context = contextlib.nullcontext()
      if is_telemetry_enabled(agent):
        span_context = tracer.start_as_current_span("invocation")

      with span_context:
        async with Aclosing(
            _run_body(new_message=new_message, invocation_id=invocation_id)
        ) as agen:
          async for e in agen:
            yield e

Add unit tests for agent and Gemini telemetry disable flags including env var gating.

Add integration test asserting spans are emitted when enabled and omitted when ADK_TELEMETRY_DISABLED is set.
Add integration coverage for OTEL_SDK_DISABLED, ADK_TELEMETRY_DISABLED, and agent.disable_telemetry.
Add back _create_gemini_cache wrapper for compatibility after telemetry refactor.

Apply autoformat across touched files.
@mportdata mportdata force-pushed the feature/disable-telemetry branch from 2c1e727 to ba0ce7a Compare December 27, 2025 23:31
Use a nullcontext fallback to avoid duplicating the async run loop when telemetry is disabled.
Use a nullcontext fallback to keep run_live telemetry handling consistent with run_async.
Use nullcontext fallbacks for tracing in base_agent and base_llm_flow.

Run autoformat on telemetry utilities.
Use a nullcontext fallback in _call_llm_with_optional_tracing to avoid branching.
Use early return when telemetry is disabled to avoid branching in execute_tool spans.
Use a nullcontext fallback for cache creation tracing.

Update GEPA sample formatting.
Restore GEPA sample files to upstream main to avoid unrelated formatting changes.
Reuse callback runner with a nullcontext span and avoid quote-only churn.
Reapply only the model telemetry toggle without quote-style churn.
Use nullcontext for live send_data/call_llm spans and skip tool tracing when telemetry is disabled.

Add a shared context_manager_with_span fixture for telemetry tests.
Keep only telemetry logic in google_llm and runners without quote/indent churn.
Use _create_gemini_cache name consistently and update tests.

Revert tracing.py quote-only churn.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tracing [Component] This issue is related to OpenTelemetry tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Disabling or Extending Internal OpenTelemetry Tracing in Google ADK (Python)

2 participants