From ae9d6266ab27939907b677230c4d181843976ac8 Mon Sep 17 00:00:00 2001 From: Subham Sinha Date: Tue, 9 Dec 2025 17:52:41 +0530 Subject: [PATCH 1/2] feat(spanner): log client configuration at startup --- google/cloud/spanner_v1/client.py | 30 +++++++++++++ tests/unit/test_client.py | 70 ++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/google/cloud/spanner_v1/client.py b/google/cloud/spanner_v1/client.py index 5f72905616..741d6b2b60 100644 --- a/google/cloud/spanner_v1/client.py +++ b/google/cloud/spanner_v1/client.py @@ -292,6 +292,36 @@ def __init__( self._nth_client_id = Client.NTH_CLIENT.increment() self._nth_request = AtomicCounter(0) + self.host = "spanner.googleapis.com" + if self._emulator_host: + self.host = self._emulator_host + elif self._experimental_host: + self.host = self._experimental_host + elif self._client_options and self._client_options.api_endpoint: + self.host = self._client_options.api_endpoint + + self._log_spanner_options() + + def _log_spanner_options(self): + """Logs Spanner client options.""" + log.info( + "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", + self.project, + self.host, + self.route_to_leader_enabled, + self._directed_read_options, + self._default_transaction_options, + self._observability_options, + _get_spanner_enable_builtin_metrics_env(), + ) + @property def _next_nth_request(self): return self._nth_request.increment() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index ab00d45268..5c8b4a21cf 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -98,7 +98,7 @@ def _constructor_test_helper( query_options=query_options, directed_read_options=directed_read_options, default_transaction_options=default_transaction_options, - **kwargs + **kwargs, ) expected_creds = expected_creds or creds.with_scopes.return_value @@ -329,6 +329,74 @@ def test_constructor_w_default_transaction_options(self): default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS, ) + def test_constructor_logs_options(self): + from google.cloud.spanner_v1 import client as MUT + + creds = build_scoped_credentials() + observability_options = {"enable_extended_tracing": True} + with self.assertLogs(MUT.__name__, level="INFO") as cm: + client = self._make_one( + project=self.PROJECT, + credentials=creds, + route_to_leader_enabled=False, + directed_read_options=self.DIRECTED_READ_OPTIONS, + default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS, + observability_options=observability_options, + ) + self.assertIsNotNone(client) + + self.assertEqual(len(cm.output), 1) + log_output = cm.output[0] + self.assertIn("Spanner options:", log_output) + self.assertIn(f"\n Project ID: {self.PROJECT}", log_output) + self.assertIn("\n Host: spanner.googleapis.com", log_output) + self.assertIn("\n Route to leader enabled: False", log_output) + self.assertIn( + f"\n Directed read options: {self.DIRECTED_READ_OPTIONS}", log_output + ) + self.assertIn( + f"\n Default transaction options: {self.DEFAULT_TRANSACTION_OPTIONS}", + log_output, + ) + self.assertIn(f"\n Observability options: {observability_options}", log_output) + # SPANNER_DISABLE_BUILTIN_METRICS is "true" from class-level patch + self.assertIn("\n Built-in metrics enabled: False", log_output) + + # Test with custom host + endpoint = "test.googleapis.com" + with self.assertLogs(MUT.__name__, level="INFO") as cm: + self._make_one( + project=self.PROJECT, + credentials=creds, + client_options={"api_endpoint": endpoint}, + ) + + self.assertEqual(len(cm.output), 1) + log_output = cm.output[0] + self.assertIn(f"\n Host: {endpoint}", log_output) + + # Test with emulator host + emulator_host = "localhost:9010" + with mock.patch.dict(os.environ, {MUT.EMULATOR_ENV_VAR: emulator_host}): + with self.assertLogs(MUT.__name__, level="INFO") as cm: + self._make_one(project=self.PROJECT) + + self.assertEqual(len(cm.output), 1) + log_output = cm.output[0] + self.assertIn(f"\n Host: {emulator_host}", log_output) + + # Test with experimental host + experimental_host = "exp.googleapis.com" + with self.assertLogs(MUT.__name__, level="INFO") as cm: + self._make_one( + project=self.PROJECT, + experimental_host=experimental_host, + ) + + self.assertEqual(len(cm.output), 1) + log_output = cm.output[0] + self.assertIn(f"\n Host: {experimental_host}", log_output) + @mock.patch("google.cloud.spanner_v1.client._get_spanner_emulator_host") def test_instance_admin_api(self, mock_em): from google.cloud.spanner_v1.client import SPANNER_ADMIN_SCOPE From 7cddd422f800ebd8b4d3b028d35c0af9edc19ba3 Mon Sep 17 00:00:00 2001 From: Subham Sinha Date: Mon, 22 Dec 2025 14:12:53 +0530 Subject: [PATCH 2/2] feat(spanner): add env variable to enable client options logging --- google/cloud/spanner_v1/client.py | 8 ++- tests/unit/test_client.py | 86 ++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/google/cloud/spanner_v1/client.py b/google/cloud/spanner_v1/client.py index 741d6b2b60..9f68e76948 100644 --- a/google/cloud/spanner_v1/client.py +++ b/google/cloud/spanner_v1/client.py @@ -75,6 +75,7 @@ _CLIENT_INFO = client_info.ClientInfo(client_library_version=__version__) EMULATOR_ENV_VAR = "SPANNER_EMULATOR_HOST" SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR = "SPANNER_DISABLE_BUILTIN_METRICS" +LOG_CLIENT_OPTIONS_ENV_VAR = "GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS" _EMULATOR_HOST_HTTP_SCHEME = ( "%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 " @@ -104,6 +105,10 @@ def _get_spanner_enable_builtin_metrics_env(): return os.getenv(SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR) != "true" +def _get_spanner_log_client_options_env(): + return os.getenv(LOG_CLIENT_OPTIONS_ENV_VAR, "false").lower() == "true" + + class Client(ClientWithProject): """Client for interacting with Cloud Spanner API. @@ -300,7 +305,8 @@ def __init__( elif self._client_options and self._client_options.api_endpoint: self.host = self._client_options.api_endpoint - self._log_spanner_options() + if _get_spanner_log_client_options_env(): + self._log_spanner_options() def _log_spanner_options(self): """Logs Spanner client options.""" diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 5c8b4a21cf..893531934c 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -329,22 +329,53 @@ def test_constructor_w_default_transaction_options(self): default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS, ) - def test_constructor_logs_options(self): + @mock.patch.dict( + os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "false"} + ) + def test_constructor_logs_options_disabled_by_default(self): from google.cloud.spanner_v1 import client as MUT + logger = MUT.log creds = build_scoped_credentials() - observability_options = {"enable_extended_tracing": True} - with self.assertLogs(MUT.__name__, level="INFO") as cm: + + with mock.patch.object(logger, "info") as info_logger: client = self._make_one( project=self.PROJECT, credentials=creds, - route_to_leader_enabled=False, - directed_read_options=self.DIRECTED_READ_OPTIONS, - default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS, - observability_options=observability_options, ) self.assertIsNotNone(client) + # Assert that no logs are emitted when the environment variable is explicitly false + info_logger.assert_not_called() + + # Also test when the environment variable is not set at all + with mock.patch.dict(os.environ, {}, clear=True): + with mock.patch.object(logger, "info") as info_logger: + client = self._make_one(project=self.PROJECT, credentials=creds) + self.assertIsNotNone(client) + # Assert that no logs are emitted when the environment variable is not set + info_logger.assert_not_called() + + def test_constructor_logs_options(self): + from google.cloud.spanner_v1 import client as MUT + + creds = build_scoped_credentials() + observability_options = {"enable_extended_tracing": True} + with self.assertLogs(MUT.__name__, level="INFO") as cm: + with mock.patch.dict( + os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"} + ): + client = self._make_one( + project=self.PROJECT, + credentials=creds, + route_to_leader_enabled=False, + directed_read_options=self.DIRECTED_READ_OPTIONS, + default_transaction_options=self.DEFAULT_TRANSACTION_OPTIONS, + observability_options=observability_options, + ) + self.assertIsNotNone(client) + # Assert that logs are emitted when the environment variable is true + # and verify their content. self.assertEqual(len(cm.output), 1) log_output = cm.output[0] self.assertIn("Spanner options:", log_output) @@ -361,38 +392,47 @@ def test_constructor_logs_options(self): self.assertIn(f"\n Observability options: {observability_options}", log_output) # SPANNER_DISABLE_BUILTIN_METRICS is "true" from class-level patch self.assertIn("\n Built-in metrics enabled: False", log_output) - # Test with custom host endpoint = "test.googleapis.com" with self.assertLogs(MUT.__name__, level="INFO") as cm: - self._make_one( - project=self.PROJECT, - credentials=creds, - client_options={"api_endpoint": endpoint}, - ) - + with mock.patch.dict( + os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"} + ): + self._make_one( + project=self.PROJECT, + credentials=creds, + client_options={"api_endpoint": endpoint}, + ) self.assertEqual(len(cm.output), 1) log_output = cm.output[0] self.assertIn(f"\n Host: {endpoint}", log_output) # Test with emulator host emulator_host = "localhost:9010" - with mock.patch.dict(os.environ, {MUT.EMULATOR_ENV_VAR: emulator_host}): + with mock.patch.dict( + os.environ, + { + MUT.EMULATOR_ENV_VAR: emulator_host, + "GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true", + }, + ): with self.assertLogs(MUT.__name__, level="INFO") as cm: - self._make_one(project=self.PROJECT) - + self._make_one(project=self.PROJECT, credentials=creds) self.assertEqual(len(cm.output), 1) log_output = cm.output[0] self.assertIn(f"\n Host: {emulator_host}", log_output) # Test with experimental host experimental_host = "exp.googleapis.com" - with self.assertLogs(MUT.__name__, level="INFO") as cm: - self._make_one( - project=self.PROJECT, - experimental_host=experimental_host, - ) - + with mock.patch.dict( + os.environ, {"GOOGLE_CLOUD_SPANNER_ENABLE_LOG_CLIENT_OPTIONS": "true"} + ): + with self.assertLogs(MUT.__name__, level="INFO") as cm: + self._make_one( + project=self.PROJECT, + credentials=creds, + experimental_host=experimental_host, + ) self.assertEqual(len(cm.output), 1) log_output = cm.output[0] self.assertIn(f"\n Host: {experimental_host}", log_output)