Skip to content
Closed
28 changes: 26 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ dependencies = [
"graphviz>=0.20.2", # Graphviz for graph rendering
"mcp>=1.5.0;python_version>='3.10'", # For MCP Toolset
"opentelemetry-api>=1.31.0", # OpenTelemetry
"opentelemetry-exporter-gcp-trace>=1.9.0",
"opentelemetry-sdk>=1.31.0",
"opentelemetry-sdk>=1.31.0", # ... and SDK (some built-in debug features built on it).
"pydantic>=2.0, <3.0.0", # For data validation/models
"python-dotenv>=1.0.0", # To manage environment variables
"PyYAML>=6.0.2", # For APIHubToolset.
Expand Down Expand Up @@ -77,6 +76,31 @@ eval = [
# go/keep-sorted end
]

# General telemetry support.
telemetry = [
# go/keep-sorted start
"opentelemetry-exporter-otlp-proto-grpc>=1.31.0",
"opentelemetry-instrumentation-google-genai>=0.1b0",
"opentelemetry-instrumentation-requests>=0.53b1",
"opentelemetry-sdk>=1.31.0",
# go/keep-sorted end
]

# Google Cloud Observability support (including
# general telemetry support).
gcp_o11y = [
# go/keep-sorted start
"google-auth>=2.39.0",
"opentelemetry-exporter-gcp-logging>=1.9.0a0",
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0",
"opentelemetry-exporter-otlp-proto-grpc>=1.31.0",
"opentelemetry-instrumentation-google-genai>=0.1b0",
"opentelemetry-instrumentation-requests>=0.53b1",
"opentelemetry-resourcedetector-gcp>=1.9.0a0",
"opentelemetry-sdk>=1.31.0",
# go/keep-sorted end
]

test = [
# go/keep-sorted start
"anthropic>=0.43.0", # For anthropic model tests
Expand Down
3 changes: 1 addition & 2 deletions src/google/adk/agents/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,20 @@
from typing import Union

from google.genai import types
from opentelemetry import trace
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field
from pydantic import field_validator
from typing_extensions import override
from typing_extensions import TypeAlias

from ..telemetry.recording import tracer
from ..events.event import Event
from .callback_context import CallbackContext

if TYPE_CHECKING:
from .invocation_context import InvocationContext

tracer = trace.get_tracer('gcp.vertex.agent')

_SingleAgentCallback: TypeAlias = Callable[
[CallbackContext],
Expand Down
36 changes: 11 additions & 25 deletions src/google/adk/cli/fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@
from google.genai import types
import graphviz
from opentelemetry import trace
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.sdk.trace import export
from opentelemetry.sdk.trace import export as otel_trace_export
from opentelemetry.sdk.trace import ReadableSpan
from opentelemetry.sdk.trace import TracerProvider
from pydantic import BaseModel
Expand All @@ -65,6 +64,7 @@
from ..sessions.in_memory_session_service import InMemorySessionService
from ..sessions.session import Session
from ..sessions.vertex_ai_session_service import VertexAiSessionService
from ..telemetry.setup import telemetry_setup
from .cli_eval import EVAL_SESSION_ID_PREFIX
from .cli_eval import EvalMetric
from .cli_eval import EvalMetricResult
Expand All @@ -78,14 +78,14 @@
_EVAL_SET_FILE_EXTENSION = ".evalset.json"


class ApiServerSpanExporter(export.SpanExporter):
class ApiServerSpanExporter(otel_trace_export.SpanExporter):

def __init__(self, trace_dict):
self.trace_dict = trace_dict

def export(
self, spans: typing.Sequence[ReadableSpan]
) -> export.SpanExportResult:
) -> otel_trace_export.SpanExportResult:
for span in spans:
if (
span.name == "call_llm"
Expand All @@ -97,7 +97,7 @@ def export(
attributes["span_id"] = span.get_span_context().span_id
if attributes.get("gcp.vertex.agent.event_id", None):
self.trace_dict[attributes["gcp.vertex.agent.event_id"]] = attributes
return export.SpanExportResult.SUCCESS
return otel_trace_export.SpanExportResult.SUCCESS

def force_flush(self, timeout_millis: int = 30000) -> bool:
return True
Expand Down Expand Up @@ -139,28 +139,14 @@ def get_fast_api_app(
trace_to_cloud: bool = False,
lifespan: Optional[Lifespan[FastAPI]] = None,
) -> FastAPI:
# InMemory tracing dict.
trace_dict: dict[str, Any] = {}

# Set up tracing in the FastAPI server.
provider = TracerProvider()
provider.add_span_processor(
export.SimpleSpanProcessor(ApiServerSpanExporter(trace_dict))
)
# Reflect options to the environment
if trace_to_cloud:
envs.load_dotenv_for_agent("", agent_dir)
if project_id := os.environ.get("GOOGLE_CLOUD_PROJECT", None):
processor = export.BatchSpanProcessor(
CloudTraceSpanExporter(project_id=project_id)
)
provider.add_span_processor(processor)
else:
logging.warning(
"GOOGLE_CLOUD_PROJECT environment variable is not set. Tracing will"
" not be enabled."
)
os.environ['ADK_TRACE_TO_CLOUD'] = 'true'

trace.set_tracer_provider(provider)
# Setup telemetry
trace_dict: dict[str, Any] = {}
trace_processor = otel_trace_export.SimpleSpanProcessor(ApiServerSpanExporter(trace_dict))
telemetry_setup.setup_telemetry(extra_trace_processors=[trace_processor])

exit_stacks = []

Expand Down
6 changes: 3 additions & 3 deletions src/google/adk/flows/llm_flows/base_llm_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
from ...models.base_llm_connection import BaseLlmConnection
from ...models.llm_request import LlmRequest
from ...models.llm_response import LlmResponse
from ...telemetry import trace_call_llm
from ...telemetry import trace_send_data
from ...telemetry import tracer
from ...telemetry.recording import trace_call_llm
from ...telemetry.recording import trace_send_data
from ...telemetry.recording import tracer
from ...tools.tool_context import ToolContext
from . import functions

Expand Down
6 changes: 3 additions & 3 deletions src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
from ...auth.auth_tool import AuthToolArguments
from ...events.event import Event
from ...events.event_actions import EventActions
from ...telemetry import trace_tool_call
from ...telemetry import trace_tool_response
from ...telemetry import tracer
from ...telemetry.recording import trace_tool_call
from ...telemetry.recording import trace_tool_response
from ...telemetry.recording import tracer
from ...tools.base_tool import BaseTool
from ...tools.tool_context import ToolContext

Expand Down
2 changes: 1 addition & 1 deletion src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from .sessions.base_session_service import BaseSessionService
from .sessions.in_memory_session_service import InMemorySessionService
from .sessions.session import Session
from .telemetry import tracer
from .telemetry.recording import tracer
from .tools.built_in_code_execution_tool import built_in_code_execution

logger = logging.getLogger(__name__)
Expand Down
13 changes: 13 additions & 0 deletions src/google/adk/telemetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
from .models.llm_response import LlmResponse


tracer = trace.get_tracer('gcp.vertex.agent')
# https://github.com/open-telemetry/semantic-conventions/blob/main/model/gen-ai/registry.yaml
# https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/#gen-ai-system
_GENAI_SYSTEM = 'gcp.gen_ai'

tracer = trace.get_tracer('gcp.google_adk')


def trace_tool_call(
Expand All @@ -45,7 +49,7 @@ def trace_tool_call(
args: The arguments to the tool call.
"""
span = trace.get_current_span()
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
span.set_attribute('gen_ai.system', _GENAI_SYSTEM)
span.set_attribute('gcp.vertex.agent.tool_call_args', json.dumps(args))


Expand All @@ -67,7 +71,7 @@ def trace_tool_response(
function response for sequential function calls.
"""
span = trace.get_current_span()
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
span.set_attribute('gen_ai.system', _GENAI_SYSTEM)
span.set_attribute(
'gcp.vertex.agent.invocation_id', invocation_context.invocation_id
)
Expand Down Expand Up @@ -106,7 +110,7 @@ def trace_call_llm(
span = trace.get_current_span()
# Special standard Open Telemetry GenaI attributes that indicate
# that this is a span related to a Generative AI system.
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
span.set_attribute('gen_ai.system', _GENAI_SYSTEM)
span.set_attribute('gen_ai.request.model', llm_request.model)
span.set_attribute(
'gcp.vertex.agent.invocation_id', invocation_context.invocation_id
Expand Down
13 changes: 13 additions & 0 deletions src/google/adk/telemetry/setup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
58 changes: 58 additions & 0 deletions src/google/adk/telemetry/setup/credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import urllib.parse


try:
import grpc as optional_grpc
except ImportError:
optional_grpc = None

try:
from google import auth as optional_google_auth
from google.auth.transport import requests as optional_google_auth_requests
from google.auth.transport import grpc as optional_google_auth_grpc
except ImportError:
optional_google_auth = None
optional_google_auth_requests = None
optional_google_auth_grpc = None


def create_gcp_api_grpc_creds():
if optional_google_auth is None:
return None
if optional_grpc is None:
return None
creds, _ = optional_google_auth.default()
request = optional_google_auth_requests.Request()
auth_metadata_plugin = optional_google_auth_grpc.AuthMetadataPlugin(
credentials=creds, request=request)
return optional_grpc.composite_channel_credentials(
optional_grpc.ssl_channel_credentials(),
optional_grpc.metadata_call_credentials(auth_metadata_plugin),
)


def _should_use_gcp_api_grpc_creds(parsed):
return (
(parsed.scheme == 'https') and
(parsed.netloc.endswith('.googleapis.com')))


def create_credentials_for_address(address):
parsed = urllib.parse.urlparse(address)
if _should_use_gcp_api_grpc_creds(parsed):
return create_gcp_api_grpc_creds()
return None
Loading