mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
feat(otel): add --otel_to_cloud experimental support
Cloud Trace, Cloud Monitoring and Cloud Logging integrations are set up via OTel if otel_to_cloud CLI param/fast_api arg is provided. This is similar to current Cloud Trace integration via trace_to_cloud, just extended to Monitoring and Logging as well. PiperOrigin-RevId: 807385680
This commit is contained in:
committed by
Copybara-Service
parent
d6d4b144e9
commit
1ae0b82f56
+12
-2
@@ -41,9 +41,12 @@ dependencies = [
|
|||||||
"google-genai>=1.21.1, <2.0.0", # Google GenAI SDK
|
"google-genai>=1.21.1, <2.0.0", # Google GenAI SDK
|
||||||
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
|
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
|
||||||
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset
|
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset
|
||||||
"opentelemetry-api>=1.31.0, <2.0.0", # OpenTelemetry
|
"opentelemetry-api>=1.31.0, <=1.37.0", # OpenTelemetry - limit upper version for sdk and api to not risk breaking changes from unstable _logs package.
|
||||||
|
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
|
||||||
|
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0, <2.0.0",
|
||||||
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
|
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
|
||||||
"opentelemetry-sdk>=1.31.0, <2.0.0",
|
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
|
||||||
|
"opentelemetry-sdk>=1.31.0, <=1.37.0",
|
||||||
"pydantic>=2.0, <3.0.0", # For data validation/models
|
"pydantic>=2.0, <3.0.0", # For data validation/models
|
||||||
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
|
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
|
||||||
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
|
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
|
||||||
@@ -147,6 +150,13 @@ extensions = [
|
|||||||
"toolbox-core>=0.1.0", # For tools.toolbox_toolset.ToolboxToolset
|
"toolbox-core>=0.1.0", # For tools.toolbox_toolset.ToolboxToolset
|
||||||
]
|
]
|
||||||
|
|
||||||
|
otel-gcp = [
|
||||||
|
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
|
||||||
|
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0, <2.0.0",
|
||||||
|
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
|
||||||
|
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
[tool.pyink]
|
[tool.pyink]
|
||||||
# Format py files following Google style-guide
|
# Format py files following Google style-guide
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import graphviz
|
|||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
from opentelemetry.sdk.trace import export as export_lib
|
from opentelemetry.sdk.trace import export as export_lib
|
||||||
from opentelemetry.sdk.trace import ReadableSpan
|
from opentelemetry.sdk.trace import ReadableSpan
|
||||||
|
from opentelemetry.sdk.trace import SpanProcessor
|
||||||
from opentelemetry.sdk.trace import TracerProvider
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@@ -257,6 +258,59 @@ class ListMetricsInfoResponse(common.BaseModel):
|
|||||||
metrics_info: list[MetricInfo]
|
metrics_info: list[MetricInfo]
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_telemetry(
|
||||||
|
otel_to_cloud: bool = False,
|
||||||
|
internal_exporters: Optional[list[SpanProcessor]] = None,
|
||||||
|
):
|
||||||
|
# TODO - remove the condition and else branch here once
|
||||||
|
# maybe_set_otel_providers is no longer experimental.
|
||||||
|
if otel_to_cloud:
|
||||||
|
_setup_telemetry_experimental(
|
||||||
|
otel_to_cloud=otel_to_cloud, internal_exporters=internal_exporters
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Old logic - to be removed when above leaves experimental.
|
||||||
|
tracer_provider = TracerProvider()
|
||||||
|
for exporter in internal_exporters:
|
||||||
|
tracer_provider.add_span_processor(exporter)
|
||||||
|
trace.set_tracer_provider(tracer_provider=tracer_provider)
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_telemetry_experimental(
|
||||||
|
otel_to_cloud: bool = False,
|
||||||
|
internal_exporters: list[SpanProcessor] = None,
|
||||||
|
):
|
||||||
|
from ..telemetry.setup import maybe_set_otel_providers
|
||||||
|
|
||||||
|
otel_hooks_to_add = []
|
||||||
|
otel_resource = None
|
||||||
|
|
||||||
|
if internal_exporters:
|
||||||
|
from ..telemetry.setup import OTelHooks
|
||||||
|
|
||||||
|
# Register ADK-specific exporters in trace provider.
|
||||||
|
otel_hooks_to_add.append(OTelHooks(span_processors=internal_exporters))
|
||||||
|
|
||||||
|
if otel_to_cloud:
|
||||||
|
from ..telemetry.google_cloud import get_gcp_exporters
|
||||||
|
from ..telemetry.google_cloud import get_gcp_resource
|
||||||
|
|
||||||
|
otel_hooks_to_add.append(
|
||||||
|
get_gcp_exporters(
|
||||||
|
# TODO - use trace_to_cloud here as well once otel_to_cloud is no
|
||||||
|
# longer experimental.
|
||||||
|
enable_cloud_tracing=True,
|
||||||
|
enable_cloud_metrics=True,
|
||||||
|
enable_cloud_logging=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
otel_resource = get_gcp_resource()
|
||||||
|
|
||||||
|
maybe_set_otel_providers(
|
||||||
|
otel_hooks_to_setup=otel_hooks_to_add, otel_resource=otel_resource
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdkWebServer:
|
class AdkWebServer:
|
||||||
"""Helper class for setting up and running the ADK web server on FastAPI.
|
"""Helper class for setting up and running the ADK web server on FastAPI.
|
||||||
|
|
||||||
@@ -355,6 +409,7 @@ class AdkWebServer:
|
|||||||
[Observer, "AdkWebServer"], None
|
[Observer, "AdkWebServer"], None
|
||||||
] = lambda o, s: None,
|
] = lambda o, s: None,
|
||||||
register_processors: Callable[[TracerProvider], None] = lambda o: None,
|
register_processors: Callable[[TracerProvider], None] = lambda o: None,
|
||||||
|
otel_to_cloud: bool = False,
|
||||||
):
|
):
|
||||||
"""Creates a FastAPI app for the ADK web server.
|
"""Creates a FastAPI app for the ADK web server.
|
||||||
|
|
||||||
@@ -371,6 +426,8 @@ class AdkWebServer:
|
|||||||
tear_down_observer: Callback for cleaning up the file system observer.
|
tear_down_observer: Callback for cleaning up the file system observer.
|
||||||
register_processors: Callback for additional Span processors to be added
|
register_processors: Callback for additional Span processors to be added
|
||||||
to the TracerProvider.
|
to the TracerProvider.
|
||||||
|
otel_to_cloud: EXPERIMENTAL. Whether to enable Cloud Trace,
|
||||||
|
Cloud Monitoring and Cloud Logging integrations.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A FastAPI app instance.
|
A FastAPI app instance.
|
||||||
@@ -395,17 +452,20 @@ class AdkWebServer:
|
|||||||
# Create tasks for all runner closures to run concurrently
|
# Create tasks for all runner closures to run concurrently
|
||||||
await cleanup.close_runners(list(self.runner_dict.values()))
|
await cleanup.close_runners(list(self.runner_dict.values()))
|
||||||
|
|
||||||
# Set up tracing in the FastAPI server.
|
|
||||||
provider = TracerProvider()
|
|
||||||
provider.add_span_processor(
|
|
||||||
export_lib.SimpleSpanProcessor(ApiServerSpanExporter(trace_dict))
|
|
||||||
)
|
|
||||||
memory_exporter = InMemoryExporter(session_trace_dict)
|
memory_exporter = InMemoryExporter(session_trace_dict)
|
||||||
provider.add_span_processor(export_lib.SimpleSpanProcessor(memory_exporter))
|
|
||||||
|
|
||||||
register_processors(provider)
|
_setup_telemetry(
|
||||||
|
otel_to_cloud=otel_to_cloud,
|
||||||
|
internal_exporters=[
|
||||||
|
export_lib.SimpleSpanProcessor(ApiServerSpanExporter(trace_dict)),
|
||||||
|
export_lib.SimpleSpanProcessor(memory_exporter),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
trace.set_tracer_provider(provider)
|
# TODO - register_processors to be removed once --otel_to_cloud is no
|
||||||
|
# longer experimental.
|
||||||
|
tracer_provider = trace.get_tracer_provider()
|
||||||
|
register_processors(tracer_provider)
|
||||||
|
|
||||||
# Run the FastAPI server.
|
# Run the FastAPI server.
|
||||||
app = FastAPI(lifespan=internal_lifespan)
|
app = FastAPI(lifespan=internal_lifespan)
|
||||||
|
|||||||
@@ -655,6 +655,17 @@ def fast_api_common_options():
|
|||||||
default=False,
|
default=False,
|
||||||
help="Optional. Whether to enable cloud trace for telemetry.",
|
help="Optional. Whether to enable cloud trace for telemetry.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--otel_to_cloud",
|
||||||
|
is_flag=True,
|
||||||
|
show_default=True,
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"EXPERIMENTAL Optional. Whether to write OTel data to Google Cloud"
|
||||||
|
" Observability services - Cloud Trace, Cloud Monitoring and Cloud"
|
||||||
|
" Logging."
|
||||||
|
),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--reload/--no-reload",
|
"--reload/--no-reload",
|
||||||
default=True,
|
default=True,
|
||||||
@@ -723,6 +734,7 @@ def cli_web(
|
|||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 8000,
|
port: int = 8000,
|
||||||
trace_to_cloud: bool = False,
|
trace_to_cloud: bool = False,
|
||||||
|
otel_to_cloud: bool = False,
|
||||||
reload: bool = True,
|
reload: bool = True,
|
||||||
session_service_uri: Optional[str] = None,
|
session_service_uri: Optional[str] = None,
|
||||||
artifact_service_uri: Optional[str] = None,
|
artifact_service_uri: Optional[str] = None,
|
||||||
@@ -776,6 +788,7 @@ def cli_web(
|
|||||||
allow_origins=allow_origins,
|
allow_origins=allow_origins,
|
||||||
web=True,
|
web=True,
|
||||||
trace_to_cloud=trace_to_cloud,
|
trace_to_cloud=trace_to_cloud,
|
||||||
|
otel_to_cloud=otel_to_cloud,
|
||||||
lifespan=_lifespan,
|
lifespan=_lifespan,
|
||||||
a2a=a2a,
|
a2a=a2a,
|
||||||
host=host,
|
host=host,
|
||||||
@@ -814,6 +827,7 @@ def cli_api_server(
|
|||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 8000,
|
port: int = 8000,
|
||||||
trace_to_cloud: bool = False,
|
trace_to_cloud: bool = False,
|
||||||
|
otel_to_cloud: bool = False,
|
||||||
reload: bool = True,
|
reload: bool = True,
|
||||||
session_service_uri: Optional[str] = None,
|
session_service_uri: Optional[str] = None,
|
||||||
artifact_service_uri: Optional[str] = None,
|
artifact_service_uri: Optional[str] = None,
|
||||||
@@ -846,6 +860,7 @@ def cli_api_server(
|
|||||||
allow_origins=allow_origins,
|
allow_origins=allow_origins,
|
||||||
web=False,
|
web=False,
|
||||||
trace_to_cloud=trace_to_cloud,
|
trace_to_cloud=trace_to_cloud,
|
||||||
|
otel_to_cloud=otel_to_cloud,
|
||||||
a2a=a2a,
|
a2a=a2a,
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def get_fast_api_app(
|
|||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 8000,
|
port: int = 8000,
|
||||||
trace_to_cloud: bool = False,
|
trace_to_cloud: bool = False,
|
||||||
|
otel_to_cloud: bool = False,
|
||||||
reload_agents: bool = False,
|
reload_agents: bool = False,
|
||||||
lifespan: Optional[Lifespan[FastAPI]] = None,
|
lifespan: Optional[Lifespan[FastAPI]] = None,
|
||||||
) -> FastAPI:
|
) -> FastAPI:
|
||||||
@@ -191,7 +192,9 @@ def get_fast_api_app(
|
|||||||
# Callbacks & other optional args for when constructing the FastAPI instance
|
# Callbacks & other optional args for when constructing the FastAPI instance
|
||||||
extra_fast_api_args = {}
|
extra_fast_api_args = {}
|
||||||
|
|
||||||
if trace_to_cloud:
|
# TODO - Remove separate trace_to_cloud logic once otel_to_cloud stops being
|
||||||
|
# EXPERIMENTAL.
|
||||||
|
if trace_to_cloud and not otel_to_cloud:
|
||||||
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
||||||
|
|
||||||
def register_processors(provider: TracerProvider) -> None:
|
def register_processors(provider: TracerProvider) -> None:
|
||||||
@@ -241,6 +244,7 @@ def get_fast_api_app(
|
|||||||
app = adk_web_server.get_fast_api_app(
|
app = adk_web_server.get_fast_api_app(
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
allow_origins=allow_origins,
|
allow_origins=allow_origins,
|
||||||
|
otel_to_cloud=otel_to_cloud,
|
||||||
**extra_fast_api_args,
|
**extra_fast_api_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ from ...events.event import Event
|
|||||||
from ...models.base_llm_connection import BaseLlmConnection
|
from ...models.base_llm_connection import BaseLlmConnection
|
||||||
from ...models.llm_request import LlmRequest
|
from ...models.llm_request import LlmRequest
|
||||||
from ...models.llm_response import LlmResponse
|
from ...models.llm_response import LlmResponse
|
||||||
from ...telemetry import trace_call_llm
|
from ...telemetry.tracing import trace_call_llm
|
||||||
from ...telemetry import trace_send_data
|
from ...telemetry.tracing import trace_send_data
|
||||||
from ...telemetry import tracer
|
from ...telemetry.tracing import tracer
|
||||||
from ...tools.base_toolset import BaseToolset
|
from ...tools.base_toolset import BaseToolset
|
||||||
from ...tools.tool_context import ToolContext
|
from ...tools.tool_context import ToolContext
|
||||||
from ...utils.context_utils import Aclosing
|
from ...utils.context_utils import Aclosing
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ from ...agents.invocation_context import InvocationContext
|
|||||||
from ...auth.auth_tool import AuthToolArguments
|
from ...auth.auth_tool import AuthToolArguments
|
||||||
from ...events.event import Event
|
from ...events.event import Event
|
||||||
from ...events.event_actions import EventActions
|
from ...events.event_actions import EventActions
|
||||||
from ...telemetry import trace_merged_tool_calls
|
from ...telemetry.tracing import trace_merged_tool_calls
|
||||||
from ...telemetry import trace_tool_call
|
from ...telemetry.tracing import trace_tool_call
|
||||||
from ...telemetry import tracer
|
from ...telemetry.tracing import tracer
|
||||||
from ...tools.base_tool import BaseTool
|
from ...tools.base_tool import BaseTool
|
||||||
from ...tools.tool_confirmation import ToolConfirmation
|
from ...tools.tool_confirmation import ToolConfirmation
|
||||||
from ...tools.tool_context import ToolContext
|
from ...tools.tool_context import ToolContext
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ from .plugins.plugin_manager import PluginManager
|
|||||||
from .sessions.base_session_service import BaseSessionService
|
from .sessions.base_session_service import BaseSessionService
|
||||||
from .sessions.in_memory_session_service import InMemorySessionService
|
from .sessions.in_memory_session_service import InMemorySessionService
|
||||||
from .sessions.session import Session
|
from .sessions.session import Session
|
||||||
from .telemetry import tracer
|
from .telemetry.tracing import tracer
|
||||||
from .tools.base_toolset import BaseToolset
|
from .tools.base_toolset import BaseToolset
|
||||||
from .utils.context_utils import Aclosing
|
from .utils.context_utils import Aclosing
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from .tracing import trace_call_llm
|
||||||
|
from .tracing import trace_merged_tool_calls
|
||||||
|
from .tracing import trace_send_data
|
||||||
|
from .tracing import trace_tool_call
|
||||||
|
from .tracing import tracer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'trace_call_llm',
|
||||||
|
'trace_merged_tool_calls',
|
||||||
|
'trace_send_data',
|
||||||
|
'trace_tool_call',
|
||||||
|
'tracer',
|
||||||
|
]
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import google.auth
|
||||||
|
from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector
|
||||||
|
from opentelemetry.sdk._logs import LogRecordProcessor
|
||||||
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||||
|
from opentelemetry.sdk.metrics.export import MetricReader
|
||||||
|
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||||
|
from opentelemetry.sdk.resources import OTELResourceDetector
|
||||||
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
from opentelemetry.sdk.trace import SpanProcessor
|
||||||
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
|
|
||||||
|
from ..utils.feature_decorator import experimental
|
||||||
|
from .setup import OTelHooks
|
||||||
|
|
||||||
|
logger = logging.getLogger('google_adk.' + __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@experimental
|
||||||
|
def get_gcp_exporters(
|
||||||
|
enable_cloud_tracing: bool = False,
|
||||||
|
enable_cloud_metrics: bool = False,
|
||||||
|
enable_cloud_logging: bool = False,
|
||||||
|
) -> OTelHooks:
|
||||||
|
"""Returns GCP OTel exporters to be used in the app.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable_tracing: whether to enable tracing to Cloud Trace.
|
||||||
|
enable_metrics: whether to enable raporting metrics to Cloud Monitoring.
|
||||||
|
enable_logging: whether to enable sending logs to Cloud Logging.
|
||||||
|
"""
|
||||||
|
_, project_id = google.auth.default()
|
||||||
|
if not project_id:
|
||||||
|
logger.warning(
|
||||||
|
'Cannot determine GCP Project. OTel GCP Exporters cannot be set up.'
|
||||||
|
' Please make sure to log into correct GCP Project.'
|
||||||
|
)
|
||||||
|
return OTelHooks()
|
||||||
|
|
||||||
|
span_processors = []
|
||||||
|
if enable_cloud_tracing:
|
||||||
|
exporter = _get_gcp_span_exporter(project_id)
|
||||||
|
span_processors.append(exporter)
|
||||||
|
|
||||||
|
metric_readers = []
|
||||||
|
if enable_cloud_metrics:
|
||||||
|
exporter = _get_gcp_metrics_exporter(project_id)
|
||||||
|
if exporter:
|
||||||
|
metric_readers.append(exporter)
|
||||||
|
|
||||||
|
log_record_processors = []
|
||||||
|
if enable_cloud_logging:
|
||||||
|
exporter = _get_gcp_logs_exporter(project_id)
|
||||||
|
if exporter:
|
||||||
|
log_record_processors.append(exporter)
|
||||||
|
|
||||||
|
return OTelHooks(
|
||||||
|
span_processors=span_processors,
|
||||||
|
metric_readers=metric_readers,
|
||||||
|
log_record_processors=log_record_processors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_gcp_span_exporter(project_id: str) -> SpanProcessor:
|
||||||
|
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
||||||
|
|
||||||
|
return BatchSpanProcessor(CloudTraceSpanExporter(project_id=project_id))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_gcp_metrics_exporter(project_id: str) -> MetricReader:
|
||||||
|
from opentelemetry.exporter.cloud_monitoring import CloudMonitoringMetricsExporter
|
||||||
|
|
||||||
|
return PeriodicExportingMetricReader(
|
||||||
|
CloudMonitoringMetricsExporter(project_id=project_id),
|
||||||
|
export_interval_millis=5000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_gcp_logs_exporter(project_id: str) -> LogRecordProcessor:
|
||||||
|
from opentelemetry.exporter.cloud_logging import CloudLoggingExporter
|
||||||
|
|
||||||
|
return BatchLogRecordProcessor(
|
||||||
|
# TODO(jawoszek) - add default_log_name once design is approved.
|
||||||
|
CloudLoggingExporter(project_id=project_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_gcp_resource() -> Resource:
|
||||||
|
# The OTELResourceDetector populates resource labels from
|
||||||
|
# environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
|
||||||
|
# Then the GCP detector adds attributes corresponding to a correct
|
||||||
|
# monitored resource if ADK runs on one of supported platforms
|
||||||
|
# (e.g. GCE, GKE, CloudRun).
|
||||||
|
return (
|
||||||
|
OTELResourceDetector()
|
||||||
|
.detect()
|
||||||
|
.merge(GoogleCloudResourceDetector(raise_on_error=False).detect())
|
||||||
|
)
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from dataclasses import field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from opentelemetry import _logs
|
||||||
|
from opentelemetry import metrics
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.sdk._logs import LoggerProvider
|
||||||
|
from opentelemetry.sdk._logs import LogRecordProcessor
|
||||||
|
from opentelemetry.sdk.metrics import MeterProvider
|
||||||
|
from opentelemetry.sdk.metrics.export import MetricReader
|
||||||
|
from opentelemetry.sdk.resources import OTELResourceDetector
|
||||||
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
from opentelemetry.sdk.trace import SpanProcessor
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
|
||||||
|
from ..utils.feature_decorator import experimental
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OTelHooks:
|
||||||
|
span_processors: list[SpanProcessor] = field(default_factory=list)
|
||||||
|
metric_readers: list[MetricReader] = field(default_factory=list)
|
||||||
|
log_record_processors: list[LogRecordProcessor] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@experimental()
|
||||||
|
def maybe_set_otel_providers(
|
||||||
|
otel_hooks_to_setup: list[OTelHooks] = None,
|
||||||
|
otel_resource: Optional[Resource] = None,
|
||||||
|
):
|
||||||
|
"""Sets up OTel providers if hooks for a given telemetry type were
|
||||||
|
passed.
|
||||||
|
|
||||||
|
If a provider for a specific telemetry type was already globally set -
|
||||||
|
this function will not override it or register more exporters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
otel_hooks_to_setup: per-telemetry-type processors and readers to be added
|
||||||
|
to OTel providers. If no hooks for a specific telemetry type are passed -
|
||||||
|
provider will not be set.
|
||||||
|
otel_resource: OTel resource to use in providers.
|
||||||
|
If empty - default OTel resource detection will be used.
|
||||||
|
"""
|
||||||
|
if otel_hooks_to_setup is None:
|
||||||
|
otel_hooks_to_setup = []
|
||||||
|
if otel_resource is None:
|
||||||
|
otel_resource = _get_otel_resource()
|
||||||
|
|
||||||
|
span_processors = []
|
||||||
|
metric_readers = []
|
||||||
|
log_record_processors = []
|
||||||
|
for otel_hooks in otel_hooks_to_setup:
|
||||||
|
for span_processor in otel_hooks.span_processors:
|
||||||
|
span_processors.append(span_processor)
|
||||||
|
for metric_reader in otel_hooks.metric_readers:
|
||||||
|
metric_readers.append(metric_reader)
|
||||||
|
for log_record_processor in otel_hooks.log_record_processors:
|
||||||
|
log_record_processors.append(log_record_processor)
|
||||||
|
|
||||||
|
# Try to set up OTel tracing.
|
||||||
|
# If the TracerProvider was already set outside of ADK, this would be a no-op
|
||||||
|
# and results in a warning. In such case we rely on user setup.
|
||||||
|
if span_processors:
|
||||||
|
new_tracer_provider = TracerProvider(resource=otel_resource)
|
||||||
|
for exporter in span_processors:
|
||||||
|
new_tracer_provider.add_span_processor(exporter)
|
||||||
|
trace.set_tracer_provider(new_tracer_provider)
|
||||||
|
|
||||||
|
# Try to set up OTel metrics.
|
||||||
|
# If the MeterProvider was already set outside of ADK, this would be a no-op
|
||||||
|
# and results in a warning. In such case we rely on user setup.
|
||||||
|
if metric_readers:
|
||||||
|
metrics.set_meter_provider(
|
||||||
|
MeterProvider(
|
||||||
|
metric_readers=metric_readers,
|
||||||
|
resource=otel_resource,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to set up OTel logging.
|
||||||
|
# If the LoggerProvider was already set outside of ADK, this would be a no-op
|
||||||
|
# and results in a warning. In such case we rely on user setup.
|
||||||
|
if log_record_processors:
|
||||||
|
new_logger_provider = LoggerProvider(
|
||||||
|
resource=otel_resource,
|
||||||
|
)
|
||||||
|
for exporter in log_record_processors:
|
||||||
|
new_logger_provider.add_log_record_processor(exporter)
|
||||||
|
_logs.set_logger_provider(new_logger_provider)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_otel_resource() -> Resource:
|
||||||
|
# The OTELResourceDetector populates resource labels from
|
||||||
|
# environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
|
||||||
|
return OTELResourceDetector().detect()
|
||||||
@@ -29,11 +29,11 @@ from typing import Any
|
|||||||
from google.genai import types
|
from google.genai import types
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
|
|
||||||
from .agents.invocation_context import InvocationContext
|
from ..agents.invocation_context import InvocationContext
|
||||||
from .events.event import Event
|
from ..events.event import Event
|
||||||
from .models.llm_request import LlmRequest
|
from ..models.llm_request import LlmRequest
|
||||||
from .models.llm_response import LlmResponse
|
from ..models.llm_response import LlmResponse
|
||||||
from .tools.base_tool import BaseTool
|
from ..tools.base_tool import BaseTool
|
||||||
|
|
||||||
tracer = trace.get_tracer('gcp.vertex.agent')
|
tracer = trace.get_tracer('gcp.vertex.agent')
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@ from typing import Dict
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from google.adk import telemetry
|
|
||||||
from google.adk.agents.llm_agent import Agent
|
from google.adk.agents.llm_agent import Agent
|
||||||
from google.adk.events.event import Event
|
from google.adk.events.event import Event
|
||||||
from google.adk.flows.llm_flows.functions import handle_function_calls_async
|
from google.adk.flows.llm_flows.functions import handle_function_calls_async
|
||||||
|
from google.adk.telemetry import tracing
|
||||||
from google.adk.tools.function_tool import FunctionTool
|
from google.adk.tools.function_tool import FunctionTool
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ async def test_simple_function_with_mocked_tracer(monkeypatch):
|
|||||||
mock_start_as_current_span_func.return_value = returned_context_manager_mock
|
mock_start_as_current_span_func.return_value = returned_context_manager_mock
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
telemetry.tracer, 'start_as_current_span', mock_start_as_current_span_func
|
tracing.tracer, 'start_as_current_span', mock_start_as_current_span_func
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_adk_trace_tool_call = mock.Mock()
|
mock_adk_trace_tool_call = mock.Mock()
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import gc
|
|||||||
import sys
|
import sys
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from google.adk import telemetry
|
|
||||||
from google.adk.agents import base_agent
|
from google.adk.agents import base_agent
|
||||||
from google.adk.agents.llm_agent import Agent
|
from google.adk.agents.llm_agent import Agent
|
||||||
from google.adk.models.base_llm import BaseLlm
|
from google.adk.models.base_llm import BaseLlm
|
||||||
|
from google.adk.telemetry import tracing
|
||||||
from google.adk.tools import FunctionTool
|
from google.adk.tools import FunctionTool
|
||||||
from google.adk.utils.context_utils import Aclosing
|
from google.adk.utils.context_utils import Aclosing
|
||||||
from google.genai.types import Part
|
from google.genai.types import Part
|
||||||
@@ -74,7 +74,7 @@ def mock_start_as_current_span(monkeypatch: pytest.MonkeyPatch) -> mock.Mock:
|
|||||||
tracer, 'start_as_current_span', mock_start_as_current_span
|
tracer, 'start_as_current_span', mock_start_as_current_span
|
||||||
)
|
)
|
||||||
|
|
||||||
do_replace(telemetry.tracer)
|
do_replace(tracing.tracer)
|
||||||
do_replace(base_agent.tracer)
|
do_replace(base_agent.tracer)
|
||||||
|
|
||||||
return mock_start_as_current_span
|
return mock_start_as_current_span
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from google.adk.telemetry.google_cloud import get_gcp_exporters
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("enable_cloud_tracing", [True, False])
|
||||||
|
@pytest.mark.parametrize("enable_cloud_metrics", [True, False])
|
||||||
|
@pytest.mark.parametrize("enable_cloud_logging", [True, False])
|
||||||
|
def test_get_gcp_exporters(
|
||||||
|
enable_cloud_tracing: bool,
|
||||||
|
enable_cloud_metrics: bool,
|
||||||
|
enable_cloud_logging: bool,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test initializing correct providers in setup_otel
|
||||||
|
when enabling telemetry via Google O11y.
|
||||||
|
"""
|
||||||
|
# Arrange.
|
||||||
|
# Mocking google.auth.default to improve the test time.
|
||||||
|
auth_mock = mock.MagicMock()
|
||||||
|
auth_mock.return_value = ("", "project-id")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"google.auth.default",
|
||||||
|
auth_mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act.
|
||||||
|
otel_hooks = get_gcp_exporters(
|
||||||
|
enable_cloud_tracing=enable_cloud_tracing,
|
||||||
|
enable_cloud_metrics=enable_cloud_metrics,
|
||||||
|
enable_cloud_logging=enable_cloud_logging,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert.
|
||||||
|
# If given telemetry type was enabled,
|
||||||
|
# the corresponding provider should be set.
|
||||||
|
assert len(otel_hooks.span_processors) == (1 if enable_cloud_tracing else 0)
|
||||||
|
assert len(otel_hooks.metric_readers) == (1 if enable_cloud_metrics else 0)
|
||||||
|
assert len(otel_hooks.log_record_processors) == (
|
||||||
|
1 if enable_cloud_logging else 0
|
||||||
|
)
|
||||||
@@ -23,9 +23,9 @@ from google.adk.agents.llm_agent import LlmAgent
|
|||||||
from google.adk.models.llm_request import LlmRequest
|
from google.adk.models.llm_request import LlmRequest
|
||||||
from google.adk.models.llm_response import LlmResponse
|
from google.adk.models.llm_response import LlmResponse
|
||||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||||
from google.adk.telemetry import trace_call_llm
|
from google.adk.telemetry.tracing import trace_call_llm
|
||||||
from google.adk.telemetry import trace_merged_tool_calls
|
from google.adk.telemetry.tracing import trace_merged_tool_calls
|
||||||
from google.adk.telemetry import trace_tool_call
|
from google.adk.telemetry.tracing import trace_tool_call
|
||||||
from google.adk.tools.base_tool import BaseTool
|
from google.adk.tools.base_tool import BaseTool
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
Reference in New Issue
Block a user