Skip to content

Commit bd76b46

Browse files
RKestcopybara-github
authored andcommitted
feat(otel): Switch CloudTraceSpanExporter to telemetry.googleapis.com
PiperOrigin-RevId: 815675872
1 parent 4b47a0a commit bd76b46

File tree

5 files changed

+98
-29
lines changed

5 files changed

+98
-29
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ dependencies = [
5252
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
5353
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
5454
"requests>=2.32.4, <3.0.0",
55-
"sqlalchemy>=2.0, <3.0.0", # SQL database ORM
5655
"sqlalchemy-spanner>=1.14.0", # Spanner database session service
56+
"sqlalchemy>=2.0, <3.0.0", # SQL database ORM
5757
"starlette>=0.46.2, <1.0.0", # For FastAPI CLI
5858
"tenacity>=8.0.0, <9.0.0", # For Retry management
5959
"typing-extensions>=4.5, <5",

src/google/adk/cli/adk_web_server.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,9 @@ def _setup_telemetry(
292292
else:
293293
# Old logic - to be removed when above leaves experimental.
294294
tracer_provider = TracerProvider()
295-
for exporter in internal_exporters:
296-
tracer_provider.add_span_processor(exporter)
295+
if internal_exporters is not None:
296+
for exporter in internal_exporters:
297+
tracer_provider.add_span_processor(exporter)
297298
trace.set_tracer_provider(tracer_provider=tracer_provider)
298299

299300

@@ -312,19 +313,24 @@ def _otel_env_vars_enabled() -> bool:
312313
def _setup_gcp_telemetry_experimental(
313314
internal_exporters: list[SpanProcessor] = None,
314315
):
315-
from ..telemetry.setup import maybe_set_otel_providers
316+
if typing.TYPE_CHECKING:
317+
from ..telemetry.setup import OTelHooks
316318

317-
otel_hooks_to_add = []
318-
otel_resource = None
319+
otel_hooks_to_add: list[OTelHooks] = []
319320

320321
if internal_exporters:
321322
from ..telemetry.setup import OTelHooks
322323

323324
# Register ADK-specific exporters in trace provider.
324325
otel_hooks_to_add.append(OTelHooks(span_processors=internal_exporters))
325326

327+
import google.auth
328+
326329
from ..telemetry.google_cloud import get_gcp_exporters
327330
from ..telemetry.google_cloud import get_gcp_resource
331+
from ..telemetry.setup import maybe_set_otel_providers
332+
333+
credentials, project_id = google.auth.default()
328334

329335
otel_hooks_to_add.append(
330336
get_gcp_exporters(
@@ -334,12 +340,14 @@ def _setup_gcp_telemetry_experimental(
334340
# TODO - reenable metrics once errors during shutdown are fixed.
335341
enable_cloud_metrics=False,
336342
enable_cloud_logging=True,
343+
google_auth=(credentials, project_id),
337344
)
338345
)
339-
otel_resource = get_gcp_resource()
346+
otel_resource = get_gcp_resource(project_id)
340347

341348
maybe_set_otel_providers(
342-
otel_hooks_to_setup=otel_hooks_to_add, otel_resource=otel_resource
349+
otel_hooks_to_setup=otel_hooks_to_add,
350+
otel_resource=otel_resource,
343351
)
344352
_setup_instrumentation_lib_if_installed()
345353

src/google/adk/telemetry/google_cloud.py

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
from __future__ import annotations
1616

1717
import logging
18+
from typing import cast
19+
from typing import Optional
20+
from typing import TYPE_CHECKING
1821

1922
import google.auth
2023
from opentelemetry.sdk._logs import LogRecordProcessor
@@ -29,6 +32,9 @@
2932
from ..utils.feature_decorator import experimental
3033
from .setup import OTelHooks
3134

35+
if TYPE_CHECKING:
36+
from google.auth.credentials import Credentials
37+
3238
logger = logging.getLogger('google_adk.' + __name__)
3339

3440

@@ -37,34 +43,43 @@ def get_gcp_exporters(
3743
enable_cloud_tracing: bool = False,
3844
enable_cloud_metrics: bool = False,
3945
enable_cloud_logging: bool = False,
46+
google_auth: Optional[tuple[Credentials, str]] = None,
4047
) -> OTelHooks:
4148
"""Returns GCP OTel exporters to be used in the app.
4249
4350
Args:
4451
enable_tracing: whether to enable tracing to Cloud Trace.
4552
enable_metrics: whether to enable raporting metrics to Cloud Monitoring.
4653
enable_logging: whether to enable sending logs to Cloud Logging.
54+
google_auth: optional custom credentials and project_id. google.auth.default() used when this is omitted.
4755
"""
48-
_, project_id = google.auth.default()
56+
57+
credentials, project_id = (
58+
google_auth if google_auth is not None else google.auth.default()
59+
)
60+
if TYPE_CHECKING:
61+
credentials = cast(Credentials, credentials)
62+
project_id = cast(str, project_id)
63+
4964
if not project_id:
5065
logger.warning(
5166
'Cannot determine GCP Project. OTel GCP Exporters cannot be set up.'
5267
' Please make sure to log into correct GCP Project.'
5368
)
5469
return OTelHooks()
5570

56-
span_processors = []
71+
span_processors: list[SpanProcessor] = []
5772
if enable_cloud_tracing:
58-
exporter = _get_gcp_span_exporter(project_id)
73+
exporter = _get_gcp_span_exporter(credentials)
5974
span_processors.append(exporter)
6075

61-
metric_readers = []
76+
metric_readers: list[MetricReader] = []
6277
if enable_cloud_metrics:
6378
exporter = _get_gcp_metrics_exporter(project_id)
6479
if exporter:
6580
metric_readers.append(exporter)
6681

67-
log_record_processors = []
82+
log_record_processors: list[LogRecordProcessor] = []
6883
if enable_cloud_logging:
6984
exporter = _get_gcp_logs_exporter(project_id)
7085
if exporter:
@@ -77,10 +92,18 @@ def get_gcp_exporters(
7792
)
7893

7994

80-
def _get_gcp_span_exporter(project_id: str) -> SpanProcessor:
81-
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
95+
def _get_gcp_span_exporter(credentials: Credentials) -> SpanProcessor:
96+
"""Adds OTEL span exporter to telemetry.googleapis.com"""
97+
98+
from google.auth.transport.requests import AuthorizedSession
99+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
82100

83-
return BatchSpanProcessor(CloudTraceSpanExporter(project_id=project_id))
101+
return BatchSpanProcessor(
102+
OTLPSpanExporter(
103+
session=AuthorizedSession(credentials=credentials),
104+
endpoint='https://telemetry.googleapis.com/v1/traces',
105+
)
106+
)
84107

85108

86109
def _get_gcp_metrics_exporter(project_id: str) -> MetricReader:
@@ -101,15 +124,22 @@ def _get_gcp_logs_exporter(project_id: str) -> LogRecordProcessor:
101124
)
102125

103126

104-
def get_gcp_resource() -> Resource:
105-
# The OTELResourceDetector populates resource labels from
106-
# environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
107-
# Then the GCP detector adds attributes corresponding to a correct
108-
# monitored resource if ADK runs on one of supported platforms
109-
# (e.g. GCE, GKE, CloudRun).
110-
111-
resource = OTELResourceDetector().detect()
127+
def get_gcp_resource(project_id: Optional[str] = None) -> Resource:
128+
"""Returns OTEL with attributes specified in the following order (attributes specified later, overwrite those specified earlier):
129+
1. Populates gcp.project_id attribute from the project_id argument if present.
130+
2. OTELResourceDetector populates resource labels from environment variables like OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
131+
3. GCP detector adds attributes corresponding to a correct monitored resource if ADK runs on one of supported platforms (e.g. GCE, GKE, CloudRun).
112132
133+
Args:
134+
project_id: project id to fill out as `gcp.project_id` on the OTEL resource.
135+
This may be overwritten by OTELResourceDetector, if `gcp.project_id` is present in `OTEL_RESOURCE_ATTRIBUTES` env var.
136+
"""
137+
resource = Resource(
138+
attributes={'gcp.project_id': project_id}
139+
if project_id is not None
140+
else {}
141+
)
142+
resource = resource.merge(OTELResourceDetector().detect())
113143
try:
114144
from opentelemetry.resourcedetector.gcp_resource_detector import GoogleCloudResourceDetector
115145

@@ -121,5 +151,4 @@ def get_gcp_resource() -> Resource:
121151
'Cloud not import opentelemetry.resourcedetector.gcp_resource_detector'
122152
' GCE, GKE or CloudRun related resource attributes may be missing'
123153
)
124-
125154
return resource

src/google/adk/telemetry/setup.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,8 @@ def maybe_set_otel_providers(
7373
otel_resource: OTel resource to use in providers.
7474
If empty - default OTel resource detection will be used.
7575
"""
76-
if otel_hooks_to_setup is None:
77-
otel_hooks_to_setup = []
78-
if otel_resource is None:
79-
otel_resource = _get_otel_resource()
76+
otel_hooks_to_setup = otel_hooks_to_setup or []
77+
otel_resource = otel_resource or _get_otel_resource()
8078

8179
# Add generic OTel exporters based on OTel env variables.
8280
otel_hooks_to_setup.append(_get_otel_exporters())

tests/unittests/telemetry/test_google_cloud.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
16+
from typing import Optional
1517
from unittest import mock
1618

1719
from google.adk.telemetry.google_cloud import get_gcp_exporters
20+
from google.adk.telemetry.google_cloud import get_gcp_resource
1821
import pytest
1922

2023

@@ -55,3 +58,34 @@ def test_get_gcp_exporters(
5558
assert len(otel_hooks.log_record_processors) == (
5659
1 if enable_cloud_logging else 0
5760
)
61+
62+
63+
@pytest.mark.parametrize("project_id_in_arg", ["project_id_in_arg", None])
64+
@pytest.mark.parametrize("project_id_on_env", ["project_id_on_env", None])
65+
def test_get_gcp_resource(
66+
project_id_in_arg: Optional[str],
67+
project_id_on_env: Optional[str],
68+
monkeypatch: pytest.MonkeyPatch,
69+
):
70+
# Arrange.
71+
if project_id_on_env is not None:
72+
monkeypatch.setenv(
73+
"OTEL_RESOURCE_ATTRIBUTES", f"gcp.project_id={project_id_on_env}"
74+
)
75+
76+
# Act.
77+
otel_resource = get_gcp_resource(project_id_in_arg)
78+
79+
# Assert.
80+
expected_project_id = (
81+
project_id_on_env
82+
if project_id_on_env is not None
83+
else project_id_in_arg
84+
if project_id_in_arg is not None
85+
else None
86+
)
87+
assert otel_resource is not None
88+
assert (
89+
otel_resource.attributes.get("gcp.project_id", None)
90+
== expected_project_id
91+
)

0 commit comments

Comments
 (0)