Skip to content

Commit a830a78

Browse files
authored
feat(spanner): log client configuration at startup (#17040)
This is an already reviewed change as part of [this PR](googleapis/python-spanner#1461) in old repo. This change introduces logging for Spanner client options upon initialization. This allows customers to easily capture and inspect the configuration being used, which is valuable for debugging and verification. This feature mirrors the existing logSpannerOptions functionality in the Java client, improving consistency across client libraries. googleapis/java-spanner#4141
1 parent e46cd6d commit a830a78

4 files changed

Lines changed: 283 additions & 5 deletions

File tree

packages/google-cloud-spanner/google/cloud/spanner_v1/_async/client.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101

102102
EMULATOR_ENV_VAR = "SPANNER_EMULATOR_HOST"
103103
SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR = "SPANNER_DISABLE_BUILTIN_METRICS"
104+
LOG_CLIENT_OPTIONS_ENV_VAR = "GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS"
104105
_EMULATOR_HOST_HTTP_SCHEME = (
105106
"%s contains a http scheme. When used with a scheme it may cause gRPC's "
106107
"DNS resolver to endlessly attempt to resolve. %s is intended to be used "
@@ -133,6 +134,10 @@ def _get_spanner_enable_builtin_metrics_env():
133134
return os.getenv(SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR) != "true"
134135

135136

137+
def _get_spanner_log_client_options_env():
138+
return os.getenv(LOG_CLIENT_OPTIONS_ENV_VAR, "false").lower() == "true"
139+
140+
136141
def _initialize_metrics(project, credentials):
137142
"""
138143
Initializes the Spanner built-in metrics.
@@ -364,6 +369,37 @@ def __init__(
364369
self._nth_client_id = Client.NTH_CLIENT.increment()
365370
self._nth_request = AtomicCounter(0)
366371

372+
self._host = "spanner.googleapis.com"
373+
if self._emulator_host:
374+
self._host = self._emulator_host
375+
elif self._experimental_host:
376+
self._host = self._experimental_host
377+
elif self._client_options and self._client_options.api_endpoint:
378+
self._host = self._client_options.api_endpoint
379+
380+
if _get_spanner_log_client_options_env():
381+
self._log_spanner_options()
382+
383+
def _log_spanner_options(self):
384+
"""Logs Spanner client options."""
385+
log.info(
386+
"Spanner options: \n"
387+
" Project ID: %s\n"
388+
" Host: %s\n"
389+
" Route to leader enabled: %s\n"
390+
" Directed read options: %s\n"
391+
" Default transaction options: %s\n"
392+
" Observability options: %s\n"
393+
" Built-in metrics enabled: %s",
394+
self.project,
395+
self._host,
396+
self.route_to_leader_enabled,
397+
self._directed_read_options,
398+
self._default_transaction_options,
399+
self._observability_options,
400+
_get_spanner_enable_builtin_metrics_env(),
401+
)
402+
367403
@property
368404
def _next_nth_request(self):
369405
return self._nth_request.increment()

packages/google-cloud-spanner/google/cloud/spanner_v1/client.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,11 @@
3232
import threading
3333
import warnings
3434
from typing import Optional
35-
3635
import google.api_core.client_options
3736
import grpc
3837
from google.api_core.gapic_v1 import client_info
3938
from google.auth.credentials import AnonymousCredentials
4039
from google.cloud.client import ClientWithProject
41-
4240
from google.cloud.spanner_admin_database_v1 import (
4341
DatabaseAdminClient as DatabaseAdminClient,
4442
)
@@ -55,14 +53,14 @@
5553
from google.cloud.spanner_admin_instance_v1.services.instance_admin.transports.grpc import (
5654
InstanceAdminGrpcTransport,
5755
)
56+
from google.cloud.spanner_v1.instance import Instance
5857
from google.cloud.spanner_v1._helpers import (
5958
AtomicCounter,
6059
_merge_query_options,
6160
_metadata_with_prefix,
6261
_validate_client_context,
6362
)
6463
from google.cloud.spanner_v1.gapic_version import __version__
65-
from google.cloud.spanner_v1.instance import Instance
6664
from google.cloud.spanner_v1.metrics.constants import METRIC_EXPORT_INTERVAL_MS
6765
from google.cloud.spanner_v1.metrics.metrics_exporter import (
6866
CloudMonitoringMetricsExporter,
@@ -84,6 +82,7 @@
8482
_CLIENT_INFO = client_info.ClientInfo(client_library_version=__version__)
8583
EMULATOR_ENV_VAR = "SPANNER_EMULATOR_HOST"
8684
SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR = "SPANNER_DISABLE_BUILTIN_METRICS"
85+
LOG_CLIENT_OPTIONS_ENV_VAR = "GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS"
8786
_EMULATOR_HOST_HTTP_SCHEME = (
8887
"%s contains a http scheme. When used with a scheme it may cause gRPC's DNS resolver to endlessly attempt to resolve. %s is intended to be used without a scheme: ex %s=localhost:8080."
8988
% ((EMULATOR_ENV_VAR,) * 3)
@@ -114,6 +113,10 @@ def _get_spanner_enable_builtin_metrics_env():
114113
return os.getenv(SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR) != "true"
115114

116115

116+
def _get_spanner_log_client_options_env():
117+
return os.getenv(LOG_CLIENT_OPTIONS_ENV_VAR, "false").lower() == "true"
118+
119+
117120
def _initialize_metrics(project, credentials):
118121
"""Initializes the Spanner built-in metrics.
119122
@@ -226,8 +229,7 @@ class Client(ClientWithProject):
226229
the Spanner built-in metrics collection and exporting.
227230
228231
:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
229-
and ``admin`` are :data:`True`
230-
"""
232+
and ``admin`` are :data:`True`"""
231233

232234
_instance_admin_api = None
233235
_database_admin_api = None
@@ -316,6 +318,28 @@ def __init__(
316318
self._default_transaction_options = default_transaction_options
317319
self._nth_client_id = Client.NTH_CLIENT.increment()
318320
self._nth_request = AtomicCounter(0)
321+
self._host = "spanner.googleapis.com"
322+
if self._emulator_host:
323+
self._host = self._emulator_host
324+
elif self._experimental_host:
325+
self._host = self._experimental_host
326+
elif self._client_options and self._client_options.api_endpoint:
327+
self._host = self._client_options.api_endpoint
328+
if _get_spanner_log_client_options_env():
329+
self._log_spanner_options()
330+
331+
def _log_spanner_options(self):
332+
"""Logs Spanner client options."""
333+
log.info(
334+
"Spanner options: \n Project ID: %s\n Host: %s\n Route to leader enabled: %s\n Directed read options: %s\n Default transaction options: %s\n Observability options: %s\n Built-in metrics enabled: %s",
335+
self.project,
336+
self._host,
337+
self.route_to_leader_enabled,
338+
self._directed_read_options,
339+
self._default_transaction_options,
340+
self._observability_options,
341+
_get_spanner_enable_builtin_metrics_env(),
342+
)
319343

320344
@property
321345
def _next_nth_request(self):

packages/google-cloud-spanner/tests/unit/_async/test_client.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,113 @@ async def __anext__(self):
778778
self.assertEqual(li_api.call_count, 1)
779779
args, kwargs = li_api.call_args
780780
self.assertEqual(kwargs["metadata"], expected_metadata)
781+
782+
@mock.patch.dict(
783+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "false"}
784+
)
785+
@CrossSync.pytest
786+
async def test_constructor_logs_options_disabled_by_default(self):
787+
from google.cloud.spanner_v1._async import client as MUT
788+
789+
logger = MUT.log
790+
creds = build_scoped_credentials()
791+
792+
with mock.patch.object(logger, "info") as info_logger:
793+
client = self._make_one(
794+
project=self.PROJECT,
795+
credentials=creds,
796+
)
797+
self.assertIsNotNone(client)
798+
# Assert that no logs are emitted when the environment variable is explicitly false
799+
info_logger.assert_not_called()
800+
801+
# Also test when the environment variable is not set at all
802+
with mock.patch.dict(os.environ, {}, clear=True):
803+
with mock.patch.object(logger, "info") as info_logger:
804+
client = self._make_one(project=self.PROJECT, credentials=creds)
805+
self.assertIsNotNone(client)
806+
# Assert that no logs are emitted when the environment variable is not set
807+
info_logger.assert_not_called()
808+
809+
@CrossSync.pytest
810+
async def test_constructor_logs_options(self):
811+
from google.cloud.spanner_v1._async import client as MUT
812+
813+
creds = build_scoped_credentials()
814+
observability_options = {"enable_extended_tracing": True}
815+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
816+
with mock.patch.dict(
817+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
818+
):
819+
client = self._make_one(
820+
project=self.PROJECT,
821+
credentials=creds,
822+
route_to_leader_enabled=False,
823+
directed_read_options=self.DIRECTED_READ_OPTIONS,
824+
default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS,
825+
observability_options=observability_options,
826+
)
827+
self.assertIsNotNone(client)
828+
829+
# Assert that logs are emitted when the environment variable is true
830+
# and verify their content.
831+
self.assertEqual(len(cm.output), 1)
832+
log_output = cm.output[0]
833+
self.assertIn("Spanner options:", log_output)
834+
self.assertIn(f"\n Project ID: {self.PROJECT}", log_output)
835+
self.assertIn("\n Host: spanner.googleapis.com", log_output)
836+
self.assertIn("\n Route to leader enabled: False", log_output)
837+
self.assertIn(
838+
f"\n Directed read options: {self.DIRECTED_READ_OPTIONS}", log_output
839+
)
840+
self.assertIn(
841+
f"\n Default transaction options: {self.DEFAULT_TRANSACTION_OPTIONS}",
842+
log_output,
843+
)
844+
self.assertIn(f"\n Observability options: {observability_options}", log_output)
845+
# SPANNER_DISABLE_BUILTIN_METRICS is "true" from class-level patch
846+
self.assertIn("\n Built-in metrics enabled: False", log_output)
847+
# Test with custom host
848+
endpoint = "test.googleapis.com"
849+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
850+
with mock.patch.dict(
851+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
852+
):
853+
self._make_one(
854+
project=self.PROJECT,
855+
credentials=creds,
856+
client_options={"api_endpoint": endpoint},
857+
)
858+
self.assertEqual(len(cm.output), 1)
859+
log_output = cm.output[0]
860+
self.assertIn(f"\n Host: {endpoint}", log_output)
861+
862+
# Test with emulator host
863+
emulator_host = "localhost:9010"
864+
with mock.patch.dict(
865+
os.environ,
866+
{
867+
MUT.EMULATOR_ENV_VAR: emulator_host,
868+
"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true",
869+
},
870+
):
871+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
872+
self._make_one(project=self.PROJECT, credentials=creds)
873+
self.assertEqual(len(cm.output), 1)
874+
log_output = cm.output[0]
875+
self.assertIn(f"\n Host: {emulator_host}", log_output)
876+
877+
# Test with experimental host
878+
experimental_host = "exp.googleapis.com"
879+
with mock.patch.dict(
880+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
881+
):
882+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
883+
self._make_one(
884+
project=self.PROJECT,
885+
credentials=creds,
886+
experimental_host=experimental_host,
887+
)
888+
self.assertEqual(len(cm.output), 1)
889+
log_output = cm.output[0]
890+
self.assertIn(f"\n Host: {experimental_host}", log_output)

packages/google-cloud-spanner/tests/unit/test_client.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,3 +836,111 @@ def test_list_instances_w_options(self):
836836
retry=mock.ANY,
837837
timeout=mock.ANY,
838838
)
839+
840+
@mock.patch.dict(
841+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "false"}
842+
)
843+
def test_constructor_logs_options_disabled_by_default(self):
844+
from google.cloud.spanner_v1 import client as MUT
845+
846+
logger = MUT.log
847+
creds = build_scoped_credentials()
848+
849+
with mock.patch.object(logger, "info") as info_logger:
850+
client = self._make_one(
851+
project=self.PROJECT,
852+
credentials=creds,
853+
)
854+
self.assertIsNotNone(client)
855+
# Assert that no logs are emitted when the environment variable is explicitly false
856+
info_logger.assert_not_called()
857+
858+
# Also test when the environment variable is not set at all
859+
with mock.patch.dict(os.environ, {}, clear=True):
860+
with mock.patch.object(logger, "info") as info_logger:
861+
client = self._make_one(project=self.PROJECT, credentials=creds)
862+
self.assertIsNotNone(client)
863+
# Assert that no logs are emitted when the environment variable is not set
864+
info_logger.assert_not_called()
865+
866+
def test_constructor_logs_options(self):
867+
from google.cloud.spanner_v1 import client as MUT
868+
869+
creds = build_scoped_credentials()
870+
observability_options = {"enable_extended_tracing": True}
871+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
872+
with mock.patch.dict(
873+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
874+
):
875+
client = self._make_one(
876+
project=self.PROJECT,
877+
credentials=creds,
878+
route_to_leader_enabled=False,
879+
directed_read_options=self.DIRECTED_READ_OPTIONS,
880+
default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS,
881+
observability_options=observability_options,
882+
)
883+
self.assertIsNotNone(client)
884+
885+
# Assert that logs are emitted when the environment variable is true
886+
# and verify their content.
887+
self.assertEqual(len(cm.output), 1)
888+
log_output = cm.output[0]
889+
self.assertIn("Spanner options:", log_output)
890+
self.assertIn(f"\n Project ID: {self.PROJECT}", log_output)
891+
self.assertIn("\n Host: spanner.googleapis.com", log_output)
892+
self.assertIn("\n Route to leader enabled: False", log_output)
893+
self.assertIn(
894+
f"\n Directed read options: {self.DIRECTED_READ_OPTIONS}", log_output
895+
)
896+
self.assertIn(
897+
f"\n Default transaction options: {self.DEFAULT_TRANSACTION_OPTIONS}",
898+
log_output,
899+
)
900+
self.assertIn(f"\n Observability options: {observability_options}", log_output)
901+
# SPANNER_DISABLE_BUILTIN_METRICS is "true" from class-level patch
902+
self.assertIn("\n Built-in metrics enabled: False", log_output)
903+
# Test with custom host
904+
endpoint = "test.googleapis.com"
905+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
906+
with mock.patch.dict(
907+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
908+
):
909+
self._make_one(
910+
project=self.PROJECT,
911+
credentials=creds,
912+
client_options={"api_endpoint": endpoint},
913+
)
914+
self.assertEqual(len(cm.output), 1)
915+
log_output = cm.output[0]
916+
self.assertIn(f"\n Host: {endpoint}", log_output)
917+
918+
# Test with emulator host
919+
emulator_host = "localhost:9010"
920+
with mock.patch.dict(
921+
os.environ,
922+
{
923+
MUT.EMULATOR_ENV_VAR: emulator_host,
924+
"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true",
925+
},
926+
):
927+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
928+
self._make_one(project=self.PROJECT, credentials=creds)
929+
self.assertEqual(len(cm.output), 1)
930+
log_output = cm.output[0]
931+
self.assertIn(f"\n Host: {emulator_host}", log_output)
932+
933+
# Test with experimental host
934+
experimental_host = "exp.googleapis.com"
935+
with mock.patch.dict(
936+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
937+
):
938+
with self.assertLogs(MUT.__name__, level="INFO") as cm:
939+
self._make_one(
940+
project=self.PROJECT,
941+
credentials=creds,
942+
experimental_host=experimental_host,
943+
)
944+
self.assertEqual(len(cm.output), 1)
945+
log_output = cm.output[0]
946+
self.assertIn(f"\n Host: {experimental_host}", log_output)

0 commit comments

Comments
 (0)