diff --git a/src/google/adk/cli/api_server.py b/src/google/adk/cli/api_server.py index 34ad0a824b..027135555e 100644 --- a/src/google/adk/cli/api_server.py +++ b/src/google/adk/cli/api_server.py @@ -694,11 +694,12 @@ def _setup_gcp_telemetry( import google.auth + from ..telemetry.google_cloud import CLOUD_PLATFORM_SCOPE from ..telemetry.google_cloud import get_gcp_exporters from ..telemetry.google_cloud import get_gcp_resource from ..telemetry.setup import maybe_set_otel_providers - credentials, project_id = google.auth.default() + credentials, project_id = google.auth.default(scopes=[CLOUD_PLATFORM_SCOPE]) otel_hooks_to_add.append( get_gcp_exporters( diff --git a/src/google/adk/telemetry/google_cloud.py b/src/google/adk/telemetry/google_cloud.py index a7f3166675..304481731e 100644 --- a/src/google/adk/telemetry/google_cloud.py +++ b/src/google/adk/telemetry/google_cloud.py @@ -82,6 +82,12 @@ "https://telemetry.mtls.googleapis.com/v1/logs" ) +# A service-account key file (unlike GCE/GKE/Cloud Run metadata-server +# credentials) has requires_scopes=True and no scopes, so google.auth.default() +# must be given a scope explicitly or the resulting credentials fail the OTLP +# exporters' token refresh with invalid_scope. +CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform" + _GCP_LOG_NAME = "gcp.log_name" _EVENT_NAME = "event.name" _GCP_RESOURCE_TYPE = "gcp.resource_type" @@ -115,7 +121,9 @@ def get_gcp_exporters( """ credentials, project_id = ( - google_auth if google_auth is not None else google.auth.default() + google_auth + if google_auth is not None + else google.auth.default(scopes=[CLOUD_PLATFORM_SCOPE]) ) if os.environ.get("GOOGLE_CLOUD_AGENT_ENGINE_ID"): # Try to convert project number to project ID to associate logs with traces. @@ -218,7 +226,9 @@ def _get_gcp_otlp_metric_exporter( from google.auth.transport.requests import AuthorizedSession credentials, _ = ( - google_auth if google_auth is not None else google.auth.default() + google_auth + if google_auth is not None + else google.auth.default(scopes=[CLOUD_PLATFORM_SCOPE]) ) session = AuthorizedSession(credentials=credentials) endpoint = _get_telemetry_endpoint( diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index a3fe28d35a..3e168c511a 100644 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -3435,6 +3435,37 @@ def test_telemetry_post_endpoint_missing_header(test_app): assert "Forbidden: missing required security header" in response.text +def test_setup_gcp_telemetry_requests_cloud_platform_scope(monkeypatch): + """A service-account key file ADC has requires_scopes=True and no scopes, + so google.auth.default() must be asked for the cloud-platform scope or the + OTLP exporters' token refresh fails with invalid_scope (issue #7024).""" + from google.adk.cli.api_server import _setup_gcp_telemetry + from google.adk.telemetry.google_cloud import CLOUD_PLATFORM_SCOPE + + auth_default = MagicMock(return_value=("creds", "project-id")) + monkeypatch.setattr("google.auth.default", auth_default) + monkeypatch.setattr( + "google.adk.telemetry.google_cloud.get_gcp_exporters", + lambda **kwargs: MagicMock(), + ) + monkeypatch.setattr( + "google.adk.telemetry.google_cloud.get_gcp_resource", + lambda project_id: MagicMock(), + ) + monkeypatch.setattr( + "google.adk.telemetry.setup.maybe_set_otel_providers", + lambda **kwargs: None, + ) + monkeypatch.setattr( + "google.adk.cli.api_server._setup_instrumentation_lib_if_installed", + lambda: None, + ) + + _setup_gcp_telemetry() + + auth_default.assert_called_once_with(scopes=[CLOUD_PLATFORM_SCOPE]) + + @pytest.fixture def test_app_auto_session( mock_session_service, diff --git a/tests/unittests/telemetry/test_google_cloud.py b/tests/unittests/telemetry/test_google_cloud.py index d4c80b5caa..d6399c006c 100644 --- a/tests/unittests/telemetry/test_google_cloud.py +++ b/tests/unittests/telemetry/test_google_cloud.py @@ -94,6 +94,10 @@ def test_get_gcp_exporters( assert len(otel_hooks.log_record_processors) == ( 1 if enable_cloud_logging else 0 ) + # A service-account key file has requires_scopes=True and no scopes; the + # scope must be requested explicitly or its token refresh fails with + # invalid_scope (see issue #7024). + auth_mock.assert_called_once_with(scopes=[google_cloud.CLOUD_PLATFORM_SCOPE]) @pytest.mark.parametrize("project_id_in_arg", ["project_id_in_arg", None]) @@ -432,9 +436,8 @@ def test_get_gcp_otlp_metric_exporter_uses_default_credentials( credentials = mock.create_autospec( google.auth.credentials.Credentials, instance=True ) - monkeypatch.setattr( - "google.auth.default", lambda: (credentials, "project-id") - ) + auth_default = mock.MagicMock(return_value=(credentials, "project-id")) + monkeypatch.setattr("google.auth.default", auth_default) session = mock.MagicMock(name="session") monkeypatch.setattr( "google.auth.transport.requests.AuthorizedSession", @@ -451,6 +454,12 @@ def test_get_gcp_otlp_metric_exporter_uses_default_credentials( ) assert _get_gcp_otlp_metric_exporter() is exporter + # A service-account key file has requires_scopes=True and no scopes; the + # scope must be requested explicitly or its token refresh fails with + # invalid_scope (see issue #7024). + auth_default.assert_called_once_with( + scopes=[google_cloud.CLOUD_PLATFORM_SCOPE] + ) def test_get_gcp_metrics_exporter_wraps_otlp_in_periodic_reader( @@ -505,7 +514,9 @@ def test_agent_engine_uses_only_request_driven_reader( """On Agent Engine there must be exactly one metric reader: two exporters would double-report every point.""" monkeypatch.delenv("GOOGLE_CLOUD_AGENT_ENGINE_ID", raising=False) - monkeypatch.setattr("google.auth.default", lambda: ("", "project-id")) + monkeypatch.setattr( + "google.auth.default", lambda **kwargs: ("", "project-id") + ) fake_state = mock.MagicMock(name="metrics_state") monkeypatch.setattr( "google.adk.telemetry.google_cloud._get_agent_engine_metrics_setup",