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
returnos.getenv(SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR) !="true"
134135

135136

137+
def_get_spanner_log_client_options_env():
138+
returnos.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+
ifself._emulator_host:
374+
self._host=self._emulator_host
375+
elifself._experimental_host:
376+
self._host=self._experimental_host
377+
elifself._client_optionsandself._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
returnself._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
importthreading
3333
importwarnings
3434
fromtypingimportOptional
35-
3635
importgoogle.api_core.client_options
3736
importgrpc
3837
fromgoogle.api_core.gapic_v1importclient_info
3938
fromgoogle.auth.credentialsimportAnonymousCredentials
4039
fromgoogle.cloud.clientimportClientWithProject
41-
4240
fromgoogle.cloud.spanner_admin_database_v1import (
4341
DatabaseAdminClientasDatabaseAdminClient,
4442
)
@@ -55,14 +53,14 @@
5553
fromgoogle.cloud.spanner_admin_instance_v1.services.instance_admin.transports.grpcimport (
5654
InstanceAdminGrpcTransport,
5755
)
56+
fromgoogle.cloud.spanner_v1.instanceimportInstance
5857
fromgoogle.cloud.spanner_v1._helpersimport (
5958
AtomicCounter,
6059
_merge_query_options,
6160
_metadata_with_prefix,
6261
_validate_client_context,
6362
)
6463
fromgoogle.cloud.spanner_v1.gapic_versionimport__version__
65-
fromgoogle.cloud.spanner_v1.instanceimportInstance
6664
fromgoogle.cloud.spanner_v1.metrics.constantsimportMETRIC_EXPORT_INTERVAL_MS
6765
fromgoogle.cloud.spanner_v1.metrics.metrics_exporterimport (
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
returnos.getenv(SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR) !="true"
115114

116115

116+
def_get_spanner_log_client_options_env():
117+
returnos.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+
ifself._emulator_host:
323+
self._host=self._emulator_host
324+
elifself._experimental_host:
325+
self._host=self._experimental_host
326+
elifself._client_optionsandself._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+
asyncdeftest_constructor_logs_options_disabled_by_default(self):
787+
fromgoogle.cloud.spanner_v1._asyncimportclientasMUT
788+
789+
logger=MUT.log
790+
creds=build_scoped_credentials()
791+
792+
withmock.patch.object(logger, "info") asinfo_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+
withmock.patch.dict(os.environ, {}, clear=True):
803+
withmock.patch.object(logger, "info") asinfo_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+
asyncdeftest_constructor_logs_options(self):
811+
fromgoogle.cloud.spanner_v1._asyncimportclientasMUT
812+
813+
creds=build_scoped_credentials()
814+
observability_options= {"enable_extended_tracing": True}
815+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
816+
withmock.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+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
850+
withmock.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+
withmock.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+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
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+
withmock.patch.dict(
880+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
881+
):
882+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
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+
deftest_constructor_logs_options_disabled_by_default(self):
844+
fromgoogle.cloud.spanner_v1importclientasMUT
845+
846+
logger=MUT.log
847+
creds=build_scoped_credentials()
848+
849+
withmock.patch.object(logger, "info") asinfo_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+
withmock.patch.dict(os.environ, {}, clear=True):
860+
withmock.patch.object(logger, "info") asinfo_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+
deftest_constructor_logs_options(self):
867+
fromgoogle.cloud.spanner_v1importclientasMUT
868+
869+
creds=build_scoped_credentials()
870+
observability_options= {"enable_extended_tracing": True}
871+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
872+
withmock.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+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
906+
withmock.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+
withmock.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+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
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+
withmock.patch.dict(
936+
os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"}
937+
):
938+
withself.assertLogs(MUT.__name__, level="INFO") ascm:
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)