From 9cd154df30335a070e9e88e89288143b9c51252b Mon Sep 17 00:00:00 2001 From: Michael Aaron Safyan Date: Wed, 16 Apr 2025 18:11:29 -0400 Subject: [PATCH 1/3] Begin work to simplify and improve telemetry setup. --- pyproject.toml | 26 +++- src/google/adk/cli/telemetry/__init__.py | 15 ++ src/google/adk/cli/telemetry/credentials.py | 42 +++++ .../telemetry/genai_sdk_instrumentation.py | 27 ++++ src/google/adk/cli/telemetry/logs_wiring.py | 13 ++ .../adk/cli/telemetry/metrics_wiring.py | 13 ++ src/google/adk/cli/telemetry/otel_resource.py | 144 ++++++++++++++++++ .../cli/telemetry/requests_instrumentation.py | 27 ++++ src/google/adk/cli/telemetry/trace_wiring.py | 14 ++ 9 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 src/google/adk/cli/telemetry/__init__.py create mode 100644 src/google/adk/cli/telemetry/credentials.py create mode 100644 src/google/adk/cli/telemetry/genai_sdk_instrumentation.py create mode 100644 src/google/adk/cli/telemetry/logs_wiring.py create mode 100644 src/google/adk/cli/telemetry/metrics_wiring.py create mode 100644 src/google/adk/cli/telemetry/otel_resource.py create mode 100644 src/google/adk/cli/telemetry/requests_instrumentation.py create mode 100644 src/google/adk/cli/telemetry/trace_wiring.py diff --git a/pyproject.toml b/pyproject.toml index 7690c7c1..d3b2fce4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,6 @@ 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", "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. @@ -77,6 +75,30 @@ 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-sdk>=1.31.0", + # go/keep-sorted end +] + test = [ # go/keep-sorted start "anthropic>=0.43.0", # For anthropic model tests diff --git a/src/google/adk/cli/telemetry/__init__.py b/src/google/adk/cli/telemetry/__init__.py new file mode 100644 index 00000000..64b0f59f --- /dev/null +++ b/src/google/adk/cli/telemetry/__init__.py @@ -0,0 +1,15 @@ +# 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. + +# This package provides utilities for initializing the telemetry. diff --git a/src/google/adk/cli/telemetry/credentials.py b/src/google/adk/cli/telemetry/credentials.py new file mode 100644 index 00000000..2290b34e --- /dev/null +++ b/src/google/adk/cli/telemetry/credentials.py @@ -0,0 +1,42 @@ +# 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. + +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_telemetry_api_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), + ) diff --git a/src/google/adk/cli/telemetry/genai_sdk_instrumentation.py b/src/google/adk/cli/telemetry/genai_sdk_instrumentation.py new file mode 100644 index 00000000..8bdfe8ba --- /dev/null +++ b/src/google/adk/cli/telemetry/genai_sdk_instrumentation.py @@ -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. + + +try: + from opentelemetry.instrumentation import google_genai as optional_google_genai_instrument_lib +except ImportError: + optional_google_genai_instrument_lib = None + + +def setup_google_genai_instrumentation() -> None: + """Initializes instrumentation of the 'google-genai' library if optional dependency is present.""" + if optional_google_genai_instrument_lib is None: + return + instrumentor = optional_google_genai_instrument_lib.GoogleGenAiSdkInstrumentor() + instrumentor.instrument() diff --git a/src/google/adk/cli/telemetry/logs_wiring.py b/src/google/adk/cli/telemetry/logs_wiring.py new file mode 100644 index 00000000..60cac4f4 --- /dev/null +++ b/src/google/adk/cli/telemetry/logs_wiring.py @@ -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. \ No newline at end of file diff --git a/src/google/adk/cli/telemetry/metrics_wiring.py b/src/google/adk/cli/telemetry/metrics_wiring.py new file mode 100644 index 00000000..60cac4f4 --- /dev/null +++ b/src/google/adk/cli/telemetry/metrics_wiring.py @@ -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. \ No newline at end of file diff --git a/src/google/adk/cli/telemetry/otel_resource.py b/src/google/adk/cli/telemetry/otel_resource.py new file mode 100644 index 00000000..f6b4f019 --- /dev/null +++ b/src/google/adk/cli/telemetry/otel_resource.py @@ -0,0 +1,144 @@ +# 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 typing import Optional + +import functools +import os + + +try: + from opentelemetry.sdk import resources as optional_otel_resources +except ImportError: + optional_otel_resources = None + +try: + from google import auth as optional_google_auth +except ImportError: + optional_google_auth = None + + +_PROJECT_ENV_VARS = [ + 'OTEL_GCLOUD_PROJECT', + 'GOOGLE_CLOUD_PROJECT', + 'GCLOUD_PROJECT', + 'GCP_PROJECT', +] + +@functools.cache +def _get_project_id() -> Optional[str]: + for env_var in _PROJECT_ENV_VARS: + from_env = os.getenv(env_var) + if from_env: + return from_env + if optional_google_auth is not None: + _, project = optional_google_auth.default() + return project + return None + + +def _get_project_with_override(override_env_var) -> Optional[str]: + project_override = os.getenv(override_env_var) + if project_override is not None: + return project_override + return _get_project_id() + + +def _get_metrics_project() -> Optional[str]: + return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_METRICS') + + +def _get_logs_project() -> Optional[str]: + return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_LOGS') + + +def _get_traces_project() -> Optional[str]: + return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_TRACES') + + +def _get_service_namespace() -> Optional[str]: + return os.getenv('OTEL_SERVICE_NAMESPACE') + + +def _get_service_name() -> Optional[str]: + return os.getenv('OTEL_SERVICE_NAME') + + +def _get_service_instance() -> Optional[str]: + return os.getenv('OTEL_SERVICE_INSTANCE_ID') + + +def _get_service_attributes() -> dict[str, str]: + result = {} + service_namespace = _get_service_namespace() + if service_namespace: + result['service.namespace.name'] = service_namespace + service_name = _get_service_name() + if service_name: + result['service.name'] = service_name + service_instance = _get_service_instance() + if service_instance: + result['service.instance.id'] = service_instance + return result + + +def _to_project_attributes(project_id: Optional[str]) -> dict[str, str]: + result = {} + if project_id: + result['gcp.project_id'] = project_id + return result + + +def _get_resource_detectors(): + if optional_otel_resources is None: + return [] + return [ + optional_otel_resources.OTELResourceDetector(), + optional_otel_resources.ProcessResourceDetector(), + optional_otel_resources.OsResourceDetector() + ] + + +def _create_resource(project_id: Optional[str]): + resource_attributes = {} + resource_attributes.update(_get_service_attributes()) + resource_attributes.update(_to_project_attributes(project_id=project_id)) + return optional_otel_resources.get_aggregated_resources( + detectors=_get_resource_detectors, + initial_resource=optional_otel_resources.Resource.create( + attributes=resource_attributes, + ) + ) + + +def get_logs_resource(): + """Returns the Open Telemetry resource to use for logs.""" + if optional_otel_resources is None: + return None + return _create_resource(_get_logs_project()) + + +def get_metrics_resource(): + """Returns the Open Telemetry resource to use for metrics.""" + if optional_otel_resources is None: + return None + return _create_resource(_get_metrics_project()) + + +def get_trace_resource(): + """Returns the Open Telemetry resource to use for traces.""" + if optional_otel_resources is None: + return None + return _create_resource(_get_traces_project()) + diff --git a/src/google/adk/cli/telemetry/requests_instrumentation.py b/src/google/adk/cli/telemetry/requests_instrumentation.py new file mode 100644 index 00000000..d9ac7988 --- /dev/null +++ b/src/google/adk/cli/telemetry/requests_instrumentation.py @@ -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. + + +try: + from opentelemetry.instrumentation import requests as optional_requests_instrument_lib +except ImportError: + optional_requests_instrument_lib = None + + +def setup_requests_instrumentation() -> None: + """Initializes instrumentation of the 'requests' library if optional dependency is present.""" + if optional_requests_instrument_lib is None: + return + instrumentor = optional_requests_instrument_lib.RequestsInstrumentor() + instrumentor.instrument() diff --git a/src/google/adk/cli/telemetry/trace_wiring.py b/src/google/adk/cli/telemetry/trace_wiring.py new file mode 100644 index 00000000..36a1e8d7 --- /dev/null +++ b/src/google/adk/cli/telemetry/trace_wiring.py @@ -0,0 +1,14 @@ +# 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 007b1bb92d5663958cf8b1bf4e4afcc1b9713bb8 Mon Sep 17 00:00:00 2001 From: Michael Aaron Safyan Date: Mon, 21 Apr 2025 15:38:40 -0400 Subject: [PATCH 2/3] Checkpoint current state. --- pyproject.toml | 2 + src/google/adk/cli/telemetry/__init__.py | 15 -- .../logs_wiring.py => telemetry/__init__.py} | 2 +- .../recording/__init__.py} | 12 +- .../setup/__init__.py} | 2 +- .../setup}/credentials.py | 18 ++- src/google/adk/telemetry/setup/env_utils.py | 108 +++++++++++++ .../adk/telemetry/setup/gcloud_project.py | 73 +++++++++ .../setup}/genai_sdk_instrumentation.py | 0 src/google/adk/telemetry/setup/logs_wiring.py | 143 ++++++++++++++++++ .../adk/telemetry/setup/metrics_wiring.py | 126 +++++++++++++++ .../setup}/otel_resource.py | 76 ++-------- .../setup}/requests_instrumentation.py | 0 .../setup}/trace_wiring.py | 0 14 files changed, 489 insertions(+), 88 deletions(-) delete mode 100644 src/google/adk/cli/telemetry/__init__.py rename src/google/adk/{cli/telemetry/logs_wiring.py => telemetry/__init__.py} (94%) rename src/google/adk/{telemetry.py => telemetry/recording/__init__.py} (93%) rename src/google/adk/{cli/telemetry/metrics_wiring.py => telemetry/setup/__init__.py} (94%) rename src/google/adk/{cli/telemetry => telemetry/setup}/credentials.py (78%) create mode 100644 src/google/adk/telemetry/setup/env_utils.py create mode 100644 src/google/adk/telemetry/setup/gcloud_project.py rename src/google/adk/{cli/telemetry => telemetry/setup}/genai_sdk_instrumentation.py (100%) create mode 100644 src/google/adk/telemetry/setup/logs_wiring.py create mode 100644 src/google/adk/telemetry/setup/metrics_wiring.py rename src/google/adk/{cli/telemetry => telemetry/setup}/otel_resource.py (58%) rename src/google/adk/{cli/telemetry => telemetry/setup}/requests_instrumentation.py (100%) rename src/google/adk/{cli/telemetry => telemetry/setup}/trace_wiring.py (100%) diff --git a/pyproject.toml b/pyproject.toml index d3b2fce4..9b471793 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +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-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. @@ -95,6 +96,7 @@ gcp_o11y = [ "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 ] diff --git a/src/google/adk/cli/telemetry/__init__.py b/src/google/adk/cli/telemetry/__init__.py deleted file mode 100644 index 64b0f59f..00000000 --- a/src/google/adk/cli/telemetry/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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. - -# This package provides utilities for initializing the telemetry. diff --git a/src/google/adk/cli/telemetry/logs_wiring.py b/src/google/adk/telemetry/__init__.py similarity index 94% rename from src/google/adk/cli/telemetry/logs_wiring.py rename to src/google/adk/telemetry/__init__.py index 60cac4f4..0a2669d7 100644 --- a/src/google/adk/cli/telemetry/logs_wiring.py +++ b/src/google/adk/telemetry/__init__.py @@ -10,4 +10,4 @@ # 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. \ No newline at end of file +# limitations under the License. diff --git a/src/google/adk/telemetry.py b/src/google/adk/telemetry/recording/__init__.py similarity index 93% rename from src/google/adk/telemetry.py rename to src/google/adk/telemetry/recording/__init__.py index 0ee6cf8e..19e975c0 100644 --- a/src/google/adk/telemetry.py +++ b/src/google/adk/telemetry/recording/__init__.py @@ -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( @@ -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)) @@ -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 ) @@ -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 diff --git a/src/google/adk/cli/telemetry/metrics_wiring.py b/src/google/adk/telemetry/setup/__init__.py similarity index 94% rename from src/google/adk/cli/telemetry/metrics_wiring.py rename to src/google/adk/telemetry/setup/__init__.py index 60cac4f4..0a2669d7 100644 --- a/src/google/adk/cli/telemetry/metrics_wiring.py +++ b/src/google/adk/telemetry/setup/__init__.py @@ -10,4 +10,4 @@ # 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. \ No newline at end of file +# limitations under the License. diff --git a/src/google/adk/cli/telemetry/credentials.py b/src/google/adk/telemetry/setup/credentials.py similarity index 78% rename from src/google/adk/cli/telemetry/credentials.py rename to src/google/adk/telemetry/setup/credentials.py index 2290b34e..b9abd585 100644 --- a/src/google/adk/cli/telemetry/credentials.py +++ b/src/google/adk/telemetry/setup/credentials.py @@ -12,6 +12,9 @@ # 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: @@ -27,7 +30,7 @@ optional_google_auth_grpc = None -def create_gcp_telemetry_api_creds(): +def create_gcp_api_grpc_creds(): if optional_google_auth is None: return None if optional_grpc is None: @@ -40,3 +43,16 @@ def create_gcp_telemetry_api_creds(): 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 diff --git a/src/google/adk/telemetry/setup/env_utils.py b/src/google/adk/telemetry/setup/env_utils.py new file mode 100644 index 00000000..13b00c31 --- /dev/null +++ b/src/google/adk/telemetry/setup/env_utils.py @@ -0,0 +1,108 @@ +# 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 os + + +def get_first_non_empty(env_vars, default_value=None): + for env_var in env_vars: + value = os.getenv(env_var) + if value: + return value + return default_value + + +def env_to_bool(env_var, default_value=False): + env_value = os.getenv(env_var) or '' + lower_env = env_value.lower() + if lower_env in ['1', 'true']: + return True + if lower_env in ['0', 'false']: + return False + return default_value + + +def get_logs_otlp_endpoint(): + return get_first_non_empty([ + # Based on https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ + 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', + 'OTEL_EXPORTER_OTLP_ENDPOINT', + ]) + + +def get_metrics_otlp_endpoint(): + return get_first_non_empty([ + # Based on https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ + 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', + 'OTEL_EXPORTER_OTLP_ENDPOINT', + ]) + + +def get_traces_otlp_endpoint(): + return get_first_non_empty([ + # Based on https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ + 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', + 'OTEL_EXPORTER_OTLP_ENDPOINT', + ]) + + +def get_logs_exporter_type(): + exporter_type = get_first_non_empty([ + # Based on https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ + 'OTLP_LOGS_EXPORTER', + 'OTLP_EXPORTER' + ]) + if exporter_type: + return exporter_type + if env_to_bool('ADK_CLOUD_O11Y'): + return 'gcp' + if env_to_bool('ADK_LOG_TO_CLOUD'): + return 'gcp' + if get_logs_otlp_endpoint(): + return 'otlp' + return None + + +def get_metrics_exporter_type(): + exporter_type = get_first_non_empty([ + # Based on https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ + 'OTLP_METRICS_EXPORTER', + 'OTLP_EXPORTER' + ]) + if exporter_type: + return exporter_type + if env_to_bool('ADK_CLOUD_O11Y'): + return 'gcp' + if env_to_bool('ADK_METRICS_TO_CLOUD'): + return 'gcp' + if get_metrics_otlp_endpoint(): + return 'otlp' + return None + + +def get_trace_exporter_type(): + exporter_type = get_first_non_empty([ + # Based on https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ + 'OTLP_TRACES_EXPORTER', + 'OTLP_EXPORTER' + ]) + if exporter_type: + return exporter_type + if env_to_bool('ADK_CLOUD_O11Y'): + return 'gcp' + if env_to_bool('ADK_TRACE_TO_CLOUD'): + return 'gcp' + if get_traces_otlp_endpoint(): + return 'otlp' + return None diff --git a/src/google/adk/telemetry/setup/gcloud_project.py b/src/google/adk/telemetry/setup/gcloud_project.py new file mode 100644 index 00000000..238a5c45 --- /dev/null +++ b/src/google/adk/telemetry/setup/gcloud_project.py @@ -0,0 +1,73 @@ +# 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 typing import Optional + +import functools +import os + +try: + from google import auth as optional_google_auth + from google.auth import exceptions as optional_google_auth_exceptions +except ImportError: + optional_google_auth = None + optional_google_auth_exceptions = None + + +_PROJECT_ENV_VARS = [ + 'OTEL_GCLOUD_PROJECT', + 'GOOGLE_CLOUD_PROJECT', + 'GCLOUD_PROJECT', + 'GCP_PROJECT', +] + + +@functools.cache +def _get_project_id() -> Optional[str]: + for env_var in _PROJECT_ENV_VARS: + from_env = os.getenv(env_var) + if from_env: + return from_env + if optional_google_auth is not None: + try: + _, project = optional_google_auth.default() + return project + except optional_google_auth_exceptions.DefaultCredentialsError: + return None + return None + + +def _get_project_with_override(override_env_var) -> Optional[str]: + project_override = os.getenv(override_env_var) + if project_override is not None: + return project_override + return _get_project_id() + + +@functools.cache +def get_metrics_project() -> Optional[str]: + """Return the Google Cloud project to which to write metrics.""" + return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_METRICS') + + +@functools.cache +def get_logs_project() -> Optional[str]: + """Return the Google Cloud project to which to write logs.""" + return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_LOGS') + + +@functools.cache +def get_traces_project() -> Optional[str]: + """Return the Google Cloud project to which to write traces.""" + return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_TRACES') diff --git a/src/google/adk/cli/telemetry/genai_sdk_instrumentation.py b/src/google/adk/telemetry/setup/genai_sdk_instrumentation.py similarity index 100% rename from src/google/adk/cli/telemetry/genai_sdk_instrumentation.py rename to src/google/adk/telemetry/setup/genai_sdk_instrumentation.py diff --git a/src/google/adk/telemetry/setup/logs_wiring.py b/src/google/adk/telemetry/setup/logs_wiring.py new file mode 100644 index 00000000..79a0f0d1 --- /dev/null +++ b/src/google/adk/telemetry/setup/logs_wiring.py @@ -0,0 +1,143 @@ +# 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 logging +import os + +from google.adk.telemetry.setup import credentials +from google.adk.telemetry.setup import env_utils +from google.adk.telemetry.setup import gcloud_project +from google.adk.telemetry.setup import otel_resource + + +try: + from opentelemetry.sdk import _logs as optional_otel_logs + from opentelemetry.sdk import _events as optional_otel_events +except ImportError: + optional_otel_logs = None + optional_otel_events = None + + +try: + from opentelemetry.exporter import cloud_logging as optional_cloud_logging_exporter +except ImportError: + optional_cloud_logging_exporter = None + + +try: + from opentelemetry.exporter.otlp.proto.grpc import _log_exporter as optional_otlp_log_exporter +except ImportError: + optional_otlp_log_exporter = None + + +_logger = logging.getLogger(__name__) + + + +def _get_default_log_name(): + return env_utils.get_first_non_empty( + [ + 'GOOGLE_CLOUD_DEFAULT_LOG_NAME', + 'GCLOUD_DEFAULT_LOG_NAME', + 'GCP_DEFAULT_LOG_NAME' + ], + default_value='google-adk-python') + + +def _create_gcp_exporter(): + if optional_otel_logs is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize logs export.') + return + if optional_cloud_logging_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-gcp-logging" dependency; cannot initialize logs export.') + return + project_id = gcloud_project.get_logs_project() + if not project_id: + _logger.warning( + 'Insufficient project information; cannot initialize logs export. ' + 'Set OTEL_GCLOUD_PROJECT_FOR_LOGS, OTEL_GCLOUD_PROJECT, or GOOGLE_CLOUD_PROJECT. ' + 'Alternatively, setup Application Default Credentials with a Service Account associated with a project.') + return + return optional_cloud_logging_exporter.CloudLoggingExporter( + project_id=project_id, + default_log_name=_get_default_log_name()) + + +def _create_otlp_exporter(): + if optional_otel_logs is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize logs export.') + return + if optional_otlp_log_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize logs export.') + address = env_utils.get_logs_otlp_endpoint() + creds = credentials.create_credentials_for_address(address) + return optional_otlp_log_exporter.OTLPLogExporter(credentials=creds) + + +_EXPORTER_FACTORIES = { + 'gcp': _create_gcp_exporter, + 'otlp': _create_otlp_exporter, +} + + +def _wrap_exporter_in_processor(exporter): + if optional_otel_logs is None: + return None + return optional_otel_logs.BatchLogRecordProcessor(exporter) + + +def _create_exporter_with_type(exporter_type): + if not exporter_type: + return None + lowercase_name = exporter_type.lower() + factory = _EXPORTER_FACTORIES.get(lowercase_name) + if factory is None: + _logger.warning('Unsupported exporter type: %s', exporter_type) + return factory() + + +def _create_processor_from_exporter_type(exporter_type): + exporter = _create_exporter_with_type(exporter_type) + if exporter is None: + return None + return _wrap_exporter_in_processor(exporter) + + +def _setup_logs_with_processors(processors): + if optional_otel_logs is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize logs export.') + return + if optional_otlp_log_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize logs export.') + project_id = gcloud_project.get_logs_project() + resource = otel_resource.get_resource(project_id=project_id) + logger_provider = optional_otel_logs.LoggerProvider(resource=resource) + for processor in processors: + logger_provider.add_log_record_processor(processor) + optional_otel_logs.set_logger_provider(logger_provider) + optional_otel_events.set_event_logger_provider(optional_otel_events.EventLoggerProvider( + logger_provider=logger_provider)) + + +def setup_logs_wiring(additional_processors = None) -> None: + processors = [] + if additional_processors: + processors.extend(additional_processors) + requested_exporter_type = env_utils.get_logs_exporter_type() + processor = _create_processor_from_exporter_type(requested_exporter_type) + if processor: + processors.append(processor) + if not processors: + return + _setup_logs_with_processors(processors) diff --git a/src/google/adk/telemetry/setup/metrics_wiring.py b/src/google/adk/telemetry/setup/metrics_wiring.py new file mode 100644 index 00000000..f398079b --- /dev/null +++ b/src/google/adk/telemetry/setup/metrics_wiring.py @@ -0,0 +1,126 @@ +# 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 logging +import os + +from google.adk.telemetry.setup import credentials +from google.adk.telemetry.setup import env_utils +from google.adk.telemetry.setup import gcloud_project +from google.adk.telemetry.setup import otel_resource + + +try: + from opentelemetry.sdk import metrics as optional_otel_metrics +except ImportError: + optional_otel_metrics = None + + +try: + from opentelemetry.exporter import cloud_monitoring as optional_cloud_monitoring_exporter +except ImportError: + optional_cloud_monitoring_exporter = None + + +try: + from opentelemetry.exporter.otlp.proto.grpc import metric_exporter as optional_otlp_metric_exporter +except ImportError: + optional_otlp_metric_exporter = None + + +_logger = logging.getLogger(__name__) + + +def _create_gcp_exporter(): + if optional_otel_metrics is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize metrics export.') + return + if optional_cloud_monitoring_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-gcp-monitoring" dependency; cannot initialize metrics export.') + return + project_id = gcloud_project.get_metrics_project() + if not project_id: + _logger.warning( + 'Insufficient project information; cannot initialize metrics export. ' + 'Set OTEL_GCLOUD_PROJECT_FOR_LOGS, OTEL_GCLOUD_PROJECT, or GOOGLE_CLOUD_PROJECT. ' + 'Alternatively, setup Application Default Credentials with a Service Account associated with a project.') + return + return optional_otlp_metric_exporter.CloudMonitoringMetricsExporter( + project_id=project_id, + add_unique_identifier=True) + + +def _create_otlp_exporter(): + if optional_otel_metrics is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize metrics export.') + return + if optional_otlp_metric_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize metrics export.') + address = env_utils.get_metrics_otlp_endpoint() + creds = credentials.create_credentials_for_address(address) + return optional_otlp_metric_exporter.OTLPMetricExporter(credentials=creds) + + +_EXPORTER_FACTORIES = { + 'gcp': _create_gcp_exporter, + 'otlp': _create_otlp_exporter, +} + + +def _wrap_exporter_in_reader(exporter): + if optional_otel_metrics is None: + return None + return optional_otel_metrics.PeriodicExportingMetricReader(exporter) + + +def _create_exporter_with_type(exporter_type): + if not exporter_type: + return None + lowercase_name = exporter_type.lower() + factory = _EXPORTER_FACTORIES.get(lowercase_name) + if factory is None: + _logger.warning('Unsupported exporter type: %s', exporter_type) + return factory() + + +def _create_reader_from_exporter_type(exporter_type): + exporter = _create_exporter_with_type(exporter_type) + if exporter is None: + return None + return _wrap_exporter_in_reader(exporter) + + +def _setup_metrics_with_readers(readers): + if optional_otel_metrics is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize metrics export.') + return + if optional_otlp_metric_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize metrics export.') + project_id = gcloud_project.get_metrics_project() + resource = otel_resource.get_resource(project_id=project_id) + meter_provider = optional_otel_metrics.MeterProvider(metric_readers=readers, resource=resource) + optional_otel_metrics.set_meter_provider(meter_provider) + + +def setup_metrics_wiring(additional_readers = None) -> None: + readers = [] + if additional_readers: + readers.extend(additional_readers) + requested_exporter_type = env_utils.get_metrics_exporter_type() + reader = _create_reader_from_exporter_type(requested_exporter_type) + if reader: + readers.append(reader) + if not readers: + return + _setup_metrics_with_readers(readers) diff --git a/src/google/adk/cli/telemetry/otel_resource.py b/src/google/adk/telemetry/setup/otel_resource.py similarity index 58% rename from src/google/adk/cli/telemetry/otel_resource.py rename to src/google/adk/telemetry/setup/otel_resource.py index f6b4f019..c579f0cb 100644 --- a/src/google/adk/cli/telemetry/otel_resource.py +++ b/src/google/adk/telemetry/setup/otel_resource.py @@ -13,8 +13,6 @@ # limitations under the License. from typing import Optional - -import functools import os @@ -24,47 +22,9 @@ optional_otel_resources = None try: - from google import auth as optional_google_auth + from opentelemetry.resourcedetector import gcp_resource_detector as optional_gcp_resource_detector except ImportError: - optional_google_auth = None - - -_PROJECT_ENV_VARS = [ - 'OTEL_GCLOUD_PROJECT', - 'GOOGLE_CLOUD_PROJECT', - 'GCLOUD_PROJECT', - 'GCP_PROJECT', -] - -@functools.cache -def _get_project_id() -> Optional[str]: - for env_var in _PROJECT_ENV_VARS: - from_env = os.getenv(env_var) - if from_env: - return from_env - if optional_google_auth is not None: - _, project = optional_google_auth.default() - return project - return None - - -def _get_project_with_override(override_env_var) -> Optional[str]: - project_override = os.getenv(override_env_var) - if project_override is not None: - return project_override - return _get_project_id() - - -def _get_metrics_project() -> Optional[str]: - return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_METRICS') - - -def _get_logs_project() -> Optional[str]: - return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_LOGS') - - -def _get_traces_project() -> Optional[str]: - return _get_project_with_override('OTEL_GCLOUD_PROJECT_FOR_TRACES') + optional_gcp_resource_detector = None def _get_service_namespace() -> Optional[str]: @@ -103,14 +63,20 @@ def _to_project_attributes(project_id: Optional[str]) -> dict[str, str]: def _get_resource_detectors(): if optional_otel_resources is None: return [] - return [ + result = [ optional_otel_resources.OTELResourceDetector(), optional_otel_resources.ProcessResourceDetector(), optional_otel_resources.OsResourceDetector() ] + if optional_gcp_resource_detector is not None: + result.append(optional_gcp_resource_detector.GoogleCloudResourceDetector()) + return result -def _create_resource(project_id: Optional[str]): +def get_resource(project_id: Optional[str] = None): + """Returns the resource to use with Open Telemetry.""" + if optional_otel_resources is None: + return None resource_attributes = {} resource_attributes.update(_get_service_attributes()) resource_attributes.update(_to_project_attributes(project_id=project_id)) @@ -120,25 +86,3 @@ def _create_resource(project_id: Optional[str]): attributes=resource_attributes, ) ) - - -def get_logs_resource(): - """Returns the Open Telemetry resource to use for logs.""" - if optional_otel_resources is None: - return None - return _create_resource(_get_logs_project()) - - -def get_metrics_resource(): - """Returns the Open Telemetry resource to use for metrics.""" - if optional_otel_resources is None: - return None - return _create_resource(_get_metrics_project()) - - -def get_trace_resource(): - """Returns the Open Telemetry resource to use for traces.""" - if optional_otel_resources is None: - return None - return _create_resource(_get_traces_project()) - diff --git a/src/google/adk/cli/telemetry/requests_instrumentation.py b/src/google/adk/telemetry/setup/requests_instrumentation.py similarity index 100% rename from src/google/adk/cli/telemetry/requests_instrumentation.py rename to src/google/adk/telemetry/setup/requests_instrumentation.py diff --git a/src/google/adk/cli/telemetry/trace_wiring.py b/src/google/adk/telemetry/setup/trace_wiring.py similarity index 100% rename from src/google/adk/cli/telemetry/trace_wiring.py rename to src/google/adk/telemetry/setup/trace_wiring.py From 01a71589a7d238744976acdf512ce4ac8f176a88 Mon Sep 17 00:00:00 2001 From: Michael Aaron Safyan Date: Mon, 21 Apr 2025 16:35:08 -0400 Subject: [PATCH 3/3] Update telemetry references. --- src/google/adk/agents/base_agent.py | 3 +- src/google/adk/cli/fast_api.py | 36 ++--- .../adk/flows/llm_flows/base_llm_flow.py | 6 +- src/google/adk/flows/llm_flows/functions.py | 6 +- src/google/adk/runners.py | 2 +- src/google/adk/telemetry/setup/env_utils.py | 2 +- src/google/adk/telemetry/setup/logs_wiring.py | 11 +- .../adk/telemetry/setup/metrics_wiring.py | 11 +- .../adk/telemetry/setup/telemetry_setup.py | 44 ++++++ .../adk/telemetry/setup/trace_wiring.py | 126 ++++++++++++++++++ 10 files changed, 200 insertions(+), 47 deletions(-) create mode 100644 src/google/adk/telemetry/setup/telemetry_setup.py diff --git a/src/google/adk/agents/base_agent.py b/src/google/adk/agents/base_agent.py index ece60fb3..0d233a83 100644 --- a/src/google/adk/agents/base_agent.py +++ b/src/google/adk/agents/base_agent.py @@ -22,20 +22,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], Optional[types.Content]] """Callback signature that is invoked before the agent run. diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index f66d8538..4e046cef 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -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 @@ -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 @@ -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" @@ -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 @@ -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 = [] diff --git a/src/google/adk/flows/llm_flows/base_llm_flow.py b/src/google/adk/flows/llm_flows/base_llm_flow.py index 188f3a5d..abc51a34 100644 --- a/src/google/adk/flows/llm_flows/base_llm_flow.py +++ b/src/google/adk/flows/llm_flows/base_llm_flow.py @@ -34,9 +34,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 diff --git a/src/google/adk/flows/llm_flows/functions.py b/src/google/adk/flows/llm_flows/functions.py index 7c5fcfb6..83acdbc8 100644 --- a/src/google/adk/flows/llm_flows/functions.py +++ b/src/google/adk/flows/llm_flows/functions.py @@ -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 diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 90419578..7f029f92 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -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__) diff --git a/src/google/adk/telemetry/setup/env_utils.py b/src/google/adk/telemetry/setup/env_utils.py index 13b00c31..777a912a 100644 --- a/src/google/adk/telemetry/setup/env_utils.py +++ b/src/google/adk/telemetry/setup/env_utils.py @@ -91,7 +91,7 @@ def get_metrics_exporter_type(): return None -def get_trace_exporter_type(): +def get_traces_exporter_type(): exporter_type = get_first_non_empty([ # Based on https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ 'OTLP_TRACES_EXPORTER', diff --git a/src/google/adk/telemetry/setup/logs_wiring.py b/src/google/adk/telemetry/setup/logs_wiring.py index 79a0f0d1..2714640d 100644 --- a/src/google/adk/telemetry/setup/logs_wiring.py +++ b/src/google/adk/telemetry/setup/logs_wiring.py @@ -58,17 +58,17 @@ def _get_default_log_name(): def _create_gcp_exporter(): if optional_otel_logs is None: _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize logs export.') - return + return None if optional_cloud_logging_exporter is None: _logger.warning('Missing "opentelemetry-exporter-gcp-logging" dependency; cannot initialize logs export.') - return + return None project_id = gcloud_project.get_logs_project() if not project_id: _logger.warning( 'Insufficient project information; cannot initialize logs export. ' 'Set OTEL_GCLOUD_PROJECT_FOR_LOGS, OTEL_GCLOUD_PROJECT, or GOOGLE_CLOUD_PROJECT. ' 'Alternatively, setup Application Default Credentials with a Service Account associated with a project.') - return + return None return optional_cloud_logging_exporter.CloudLoggingExporter( project_id=project_id, default_log_name=_get_default_log_name()) @@ -77,9 +77,10 @@ def _create_gcp_exporter(): def _create_otlp_exporter(): if optional_otel_logs is None: _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize logs export.') - return + return None if optional_otlp_log_exporter is None: _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize logs export.') + return None address = env_utils.get_logs_otlp_endpoint() creds = credentials.create_credentials_for_address(address) return optional_otlp_log_exporter.OTLPLogExporter(credentials=creds) @@ -118,8 +119,6 @@ def _setup_logs_with_processors(processors): if optional_otel_logs is None: _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize logs export.') return - if optional_otlp_log_exporter is None: - _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize logs export.') project_id = gcloud_project.get_logs_project() resource = otel_resource.get_resource(project_id=project_id) logger_provider = optional_otel_logs.LoggerProvider(resource=resource) diff --git a/src/google/adk/telemetry/setup/metrics_wiring.py b/src/google/adk/telemetry/setup/metrics_wiring.py index f398079b..25a618c2 100644 --- a/src/google/adk/telemetry/setup/metrics_wiring.py +++ b/src/google/adk/telemetry/setup/metrics_wiring.py @@ -45,17 +45,17 @@ def _create_gcp_exporter(): if optional_otel_metrics is None: _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize metrics export.') - return + return None if optional_cloud_monitoring_exporter is None: _logger.warning('Missing "opentelemetry-exporter-gcp-monitoring" dependency; cannot initialize metrics export.') - return + return None project_id = gcloud_project.get_metrics_project() if not project_id: _logger.warning( 'Insufficient project information; cannot initialize metrics export. ' 'Set OTEL_GCLOUD_PROJECT_FOR_LOGS, OTEL_GCLOUD_PROJECT, or GOOGLE_CLOUD_PROJECT. ' 'Alternatively, setup Application Default Credentials with a Service Account associated with a project.') - return + return None return optional_otlp_metric_exporter.CloudMonitoringMetricsExporter( project_id=project_id, add_unique_identifier=True) @@ -64,9 +64,10 @@ def _create_gcp_exporter(): def _create_otlp_exporter(): if optional_otel_metrics is None: _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize metrics export.') - return + return None if optional_otlp_metric_exporter is None: _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize metrics export.') + return None address = env_utils.get_metrics_otlp_endpoint() creds = credentials.create_credentials_for_address(address) return optional_otlp_metric_exporter.OTLPMetricExporter(credentials=creds) @@ -105,8 +106,6 @@ def _setup_metrics_with_readers(readers): if optional_otel_metrics is None: _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize metrics export.') return - if optional_otlp_metric_exporter is None: - _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize metrics export.') project_id = gcloud_project.get_metrics_project() resource = otel_resource.get_resource(project_id=project_id) meter_provider = optional_otel_metrics.MeterProvider(metric_readers=readers, resource=resource) diff --git a/src/google/adk/telemetry/setup/telemetry_setup.py b/src/google/adk/telemetry/setup/telemetry_setup.py new file mode 100644 index 00000000..20463118 --- /dev/null +++ b/src/google/adk/telemetry/setup/telemetry_setup.py @@ -0,0 +1,44 @@ +# 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 google.adk.telemetry.setup import genai_sdk_instrumentation +from google.adk.telemetry.setup import requests_instrumentation +from google.adk.telemetry.setup import logs_wiring +from google.adk.telemetry.setup import metrics_wiring +from google.adk.telemetry.setup import trace_wiring + + +def _setup_instrumentation(): + genai_sdk_instrumentation.setup_google_genai_instrumentation() + requests_instrumentation.setup_requests_instrumentation() + + +def _setup_wiring( + extra_trace_processors = None, + extra_logs_processors = None, + extra_metric_readers = None): + logs_wiring.setup_logs_wiring(additional_processors=extra_logs_processors) + metrics_wiring.setup_metrics_wiring(additional_readers=extra_metric_readers) + trace_wiring.setup_traces_wiring(additional_processors=extra_trace_processors) + + +def setup_telemetry( + extra_trace_processors = None, + extra_logs_processors = None, + extra_metric_readers = None): + _setup_instrumentation() + _setup_wiring( + extra_trace_processors=extra_trace_processors, + extra_logs_processors=extra_logs_processors, + extra_metric_readers=extra_metric_readers) diff --git a/src/google/adk/telemetry/setup/trace_wiring.py b/src/google/adk/telemetry/setup/trace_wiring.py index 36a1e8d7..3622812f 100644 --- a/src/google/adk/telemetry/setup/trace_wiring.py +++ b/src/google/adk/telemetry/setup/trace_wiring.py @@ -12,3 +12,129 @@ # See the License for the specific language governing permissions and # limitations under the License. +# 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 logging +import os + +from google.adk.telemetry.setup import credentials +from google.adk.telemetry.setup import env_utils +from google.adk.telemetry.setup import gcloud_project +from google.adk.telemetry.setup import otel_resource + + +try: + from opentelemetry.sdk import traces as optional_otel_traces +except ImportError: + optional_otel_traces = None + + +try: + from opentelemetry.exporter.otlp.proto.grpc import trace_exporter as optional_otlp_trace_exporter +except ImportError: + optional_otlp_trace_exporter = None + + +_logger = logging.getLogger(__name__) + + +def _create_gcp_exporter(): + if optional_otel_traces is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize trace export.') + return + if optional_otlp_trace_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize trace export.') + address = env_utils.get_traces_otlp_endpoint() + creds = credentials.create_credentials_for_address(address) + return optional_otlp_trace_exporter.OTLPSpanExporter(credentials=creds) + + +def _create_otlp_exporter(): + if optional_otel_traces is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize trace export.') + return None + if optional_otlp_trace_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize trace export.') + return None + address = env_utils.get_traces_otlp_endpoint() + creds = credentials.create_credentials_for_address(address) + return optional_otlp_trace_exporter.OTLPSpanExporter(credentials=creds) + + +def _create_gcp_exporter(): + if optional_otel_traces is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize trace export.') + return None + if optional_otlp_trace_exporter is None: + _logger.warning('Missing "opentelemetry-exporter-otlp-proto-grpc" dependency; cannot initialize trace export.') + return None + creds = credentials.create_gcp_api_grpc_creds() + return optional_otlp_trace_exporter.OTLPSpanExporter( + endpoint='https://telemetry.googleapis.com/', + credentials=creds) + + +_EXPORTER_FACTORIES = { + 'gcp': _create_gcp_exporter, + 'otlp': _create_otlp_exporter, +} + + +def _wrap_exporter_in_processor(exporter): + if optional_otel_traces is None: + return None + return optional_otel_traces.BatchSpanProcessor(exporter) + + +def _create_exporter_with_type(exporter_type): + if not exporter_type: + return None + lowercase_name = exporter_type.lower() + factory = _EXPORTER_FACTORIES.get(lowercase_name) + if factory is None: + _logger.warning('Unsupported exporter type: %s', exporter_type) + return factory() + + +def _create_processor_from_exporter_type(exporter_type): + exporter = _create_exporter_with_type(exporter_type) + if exporter is None: + return None + return _wrap_exporter_in_processor(exporter) + + +def _setup_traces_with_processors(processors): + if optional_otel_traces is None: + _logger.warning('Missing "opentelemetry-sdk" dependency; cannot initialize trace export.') + return + project_id = gcloud_project.get_traces_project() + resource = otel_resource.get_resource(project_id=project_id) + tracer_provider = optional_otel_traces.TracerProvider(resource=resource) + for processor in processors: + tracer_provider.add_span_processor(processor) + optional_otel_traces.set_tracer_provider(tracer_provider) + + +def setup_traces_wiring(additional_processors = None) -> None: + processors = [] + if additional_processors: + processors.extend(additional_processors) + requested_exporter_type = env_utils.get_traces_exporter_type() + processor = _create_processor_from_exporter_type(requested_exporter_type) + if processor: + processors.append(processor) + if not processors: + return + _setup_traces_with_processors(processors)