Skip to content

Commit 7ef9fb9

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Add mTLS support to Google Cloud Telemetry exporter
This change enables the Google Cloud Telemetry exporter to use mTLS endpoints. It checks for the availability of client certificates and respects the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variables to determine whether to use the mTLS-specific endpoint and configure the session accordingly. PiperOrigin-RevId: 911581237
1 parent 153c7f7 commit 7ef9fb9

2 files changed

Lines changed: 174 additions & 2 deletions

File tree

src/google/adk/telemetry/google_cloud.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
from __future__ import annotations
1616

17+
import enum
1718
import logging
1819
import os
1920
from typing import cast
2021
from typing import Optional
2122
from typing import TYPE_CHECKING
2223

2324
import google.auth
25+
from google.auth.transport import mtls
2426
from opentelemetry.sdk._logs import LogRecordProcessor
2527
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
2628
from opentelemetry.sdk.metrics.export import MetricReader
@@ -40,6 +42,19 @@
4042
_GCP_LOG_NAME_ENV_VARIABLE_NAME = 'GOOGLE_CLOUD_DEFAULT_LOG_NAME'
4143
_DEFAULT_LOG_NAME = 'adk-otel'
4244

45+
_DEFAULT_TELEMETRY_ENDPOINT = 'https://telemetry.googleapis.com/v1/traces'
46+
_DEFAULT_MTLS_TELEMETRY_ENDPOINT = (
47+
'https://telemetry.mtls.googleapis.com/v1/traces'
48+
)
49+
50+
51+
class MtlsEndpoint(enum.Enum):
52+
"""Enum for the mTLS endpoint setting."""
53+
54+
AUTO = 'auto'
55+
ALWAYS = 'always'
56+
NEVER = 'never'
57+
4358

4459
def get_gcp_exporters(
4560
enable_cloud_tracing: bool = False,
@@ -100,10 +115,24 @@ def _get_gcp_span_exporter(credentials: Credentials) -> SpanProcessor:
100115
from google.auth.transport.requests import AuthorizedSession
101116
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
102117

118+
session = AuthorizedSession(credentials=credentials)
119+
120+
use_client_cert = _use_client_cert_effective()
121+
if use_client_cert:
122+
client_cert_source = (
123+
mtls.default_client_cert_source()
124+
if mtls.has_default_client_cert_source()
125+
else None
126+
)
127+
session.configure_mtls_channel()
128+
endpoint = _get_api_endpoint(client_cert_source)
129+
else:
130+
endpoint = _DEFAULT_TELEMETRY_ENDPOINT
131+
103132
return BatchSpanProcessor(
104133
OTLPSpanExporter(
105-
session=AuthorizedSession(credentials=credentials),
106-
endpoint='https://telemetry.googleapis.com/v1/traces',
134+
session=session,
135+
endpoint=endpoint,
107136
)
108137
)
109138

@@ -158,3 +187,58 @@ def get_gcp_resource(project_id: Optional[str] = None) -> Resource:
158187
' GCE, GKE or CloudRun related resource attributes may be missing'
159188
)
160189
return resource
190+
191+
192+
def _get_api_endpoint(client_cert_source: bytes | None = None) -> str:
193+
"""Returns API endpoint based on mTLS configuration and cert availability.
194+
195+
Args:
196+
client_cert_source (bytes | None): The client certificate source.
197+
198+
Returns:
199+
str: The API endpoint to be used.
200+
"""
201+
use_mtls_endpoint_str = os.getenv(
202+
'GOOGLE_API_USE_MTLS_ENDPOINT', MtlsEndpoint.AUTO.value
203+
).lower()
204+
205+
try:
206+
use_mtls_endpoint = MtlsEndpoint(use_mtls_endpoint_str)
207+
except ValueError:
208+
logger.warning(
209+
'Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be one of '
210+
f'{[e.value for e in MtlsEndpoint]}. Defaulting to'
211+
f' {MtlsEndpoint.AUTO.value}.'
212+
)
213+
use_mtls_endpoint = MtlsEndpoint.AUTO
214+
215+
if (use_mtls_endpoint == MtlsEndpoint.ALWAYS) or (
216+
use_mtls_endpoint == MtlsEndpoint.AUTO and client_cert_source
217+
):
218+
return _DEFAULT_MTLS_TELEMETRY_ENDPOINT
219+
220+
return _DEFAULT_TELEMETRY_ENDPOINT
221+
222+
223+
def _use_client_cert_effective() -> bool:
224+
"""Returns whether client certificate should be used for mTLS.
225+
226+
This checks if the google-auth version supports should_use_client_cert
227+
automatic mTLS enablement. Alternatively, it reads from the
228+
GOOGLE_API_USE_CLIENT_CERTIFICATE env var.
229+
230+
Returns:
231+
bool: whether client certificate should be used for mTLS.
232+
"""
233+
try:
234+
return mtls.should_use_client_cert()
235+
except (ImportError, AttributeError):
236+
use_client_cert_str = os.getenv(
237+
'GOOGLE_API_USE_CLIENT_CERTIFICATE', 'false'
238+
).lower()
239+
if use_client_cert_str not in ('true', 'false'):
240+
logger.warning(
241+
'Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be'
242+
' either `true` or `false`'
243+
)
244+
return use_client_cert_str == 'true'

tests/unittests/telemetry/test_google_cloud.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
from typing import Optional
1717
from unittest import mock
1818

19+
from google.adk.telemetry.google_cloud import _DEFAULT_MTLS_TELEMETRY_ENDPOINT
20+
from google.adk.telemetry.google_cloud import _DEFAULT_TELEMETRY_ENDPOINT
21+
from google.adk.telemetry.google_cloud import _get_api_endpoint
22+
from google.adk.telemetry.google_cloud import _get_gcp_span_exporter
23+
from google.adk.telemetry.google_cloud import _use_client_cert_effective
1924
from google.adk.telemetry.google_cloud import get_gcp_exporters
2025
from google.adk.telemetry.google_cloud import get_gcp_resource
2126
import pytest
@@ -89,3 +94,86 @@ def test_get_gcp_resource(
8994
otel_resource.attributes.get("gcp.project_id", None)
9095
== expected_project_id
9196
)
97+
98+
99+
@mock.patch("google.auth.transport.mtls.should_use_client_cert")
100+
def test_use_client_cert_effective_from_mtls(mock_should_use):
101+
mock_should_use.return_value = True
102+
assert _use_client_cert_effective()
103+
104+
mock_should_use.return_value = False
105+
assert not _use_client_cert_effective()
106+
107+
108+
def test_use_client_cert_effective_from_env(monkeypatch, caplog):
109+
with mock.patch(
110+
"google.auth.transport.mtls.should_use_client_cert",
111+
side_effect=AttributeError,
112+
):
113+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true")
114+
assert _use_client_cert_effective()
115+
116+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")
117+
assert not _use_client_cert_effective()
118+
119+
# Test invalid value defaults to False
120+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "maybe")
121+
assert not _use_client_cert_effective()
122+
assert (
123+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
124+
" either `true` or `false`"
125+
in caplog.text
126+
)
127+
128+
129+
@pytest.mark.parametrize(
130+
"env_val, cert_source, expected",
131+
[
132+
("auto", b"cert", _DEFAULT_MTLS_TELEMETRY_ENDPOINT),
133+
("auto", None, _DEFAULT_TELEMETRY_ENDPOINT),
134+
("always", None, _DEFAULT_MTLS_TELEMETRY_ENDPOINT),
135+
("never", b"cert", _DEFAULT_TELEMETRY_ENDPOINT),
136+
("invalid", None, _DEFAULT_TELEMETRY_ENDPOINT),
137+
],
138+
)
139+
def test_get_api_endpoint(env_val, cert_source, expected, monkeypatch, caplog):
140+
monkeypatch.setenv("GOOGLE_API_USE_MTLS_ENDPOINT", env_val)
141+
if env_val == "invalid":
142+
assert _get_api_endpoint(cert_source) == expected
143+
assert (
144+
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be one of"
145+
in caplog.text
146+
)
147+
else:
148+
assert _get_api_endpoint(cert_source) == expected
149+
150+
151+
@mock.patch("google.auth.transport.requests.AuthorizedSession")
152+
@mock.patch(
153+
"opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter"
154+
)
155+
@mock.patch("opentelemetry.sdk.trace.export.BatchSpanProcessor")
156+
@mock.patch("google.adk.telemetry.google_cloud._use_client_cert_effective")
157+
@mock.patch("google.auth.transport.mtls.has_default_client_cert_source")
158+
@mock.patch("google.auth.transport.mtls.default_client_cert_source")
159+
def test_get_gcp_span_exporter_mtls(
160+
mock_default_cert,
161+
mock_has_cert,
162+
mock_use_cert,
163+
mock_batch,
164+
mock_exporter,
165+
mock_session,
166+
):
167+
credentials = mock.Mock()
168+
mock_use_cert.return_value = True
169+
mock_has_cert.return_value = True
170+
mock_default_cert.return_value = b"cert"
171+
172+
_get_gcp_span_exporter(credentials)
173+
174+
mock_session.assert_called_once_with(credentials=credentials)
175+
mock_session.return_value.configure_mtls_channel.assert_called_once()
176+
mock_exporter.assert_called_once_with(
177+
session=mock_session.return_value,
178+
endpoint=_DEFAULT_MTLS_TELEMETRY_ENDPOINT,
179+
)

0 commit comments

Comments
 (0)