Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/google/adk/cli/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 12 additions & 2 deletions src/google/adk/telemetry/google_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
31 changes: 31 additions & 0 deletions tests/unittests/cli/test_fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 15 additions & 4 deletions tests/unittests/telemetry/test_google_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand Down