Skip to content

Commit cbe95d4

Browse files
committed
feat(spanner): log client configuration at startup
1 parent 471eb13 commit cbe95d4

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)