Skip to content

feat: Add support for further telemetry collection with additional signal types and with additional initialization libraries. #320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
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 @@ -23,20 +23,19 @@
from typing import TYPE_CHECKING

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 ..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')

BeforeAgentCallback = 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