Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions google/cloud/spanner_v1/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import os
import logging
import warnings
import threading

from google.api_core.gapic_v1 import client_info
from google.auth.credentials import AnonymousCredentials
Expand Down Expand Up @@ -99,6 +100,9 @@ def _get_spanner_optimizer_statistics_package():

log = logging.getLogger(__name__)

_metrics_monitor_initialized = False
_metrics_monitor_lock = threading.Lock()


def _get_spanner_enable_builtin_metrics_env():
return os.getenv(SPANNER_DISABLE_BUILTIN_METRICS_ENV_VAR) != "true"
Expand Down Expand Up @@ -252,30 +256,37 @@ def __init__(
):
warnings.warn(_EMULATOR_HOST_HTTP_SCHEME)
# Check flag to enable Spanner builtin metrics
global _metrics_monitor_initialized
if (
_get_spanner_enable_builtin_metrics_env()
and not disable_builtin_metrics
and HAS_GOOGLE_CLOUD_MONITORING_INSTALLED
):
meter_provider = metrics.NoOpMeterProvider()
try:
if not _get_spanner_emulator_host():
meter_provider = MeterProvider(
metric_readers=[
PeriodicExportingMetricReader(
CloudMonitoringMetricsExporter(
project_id=project, credentials=credentials
),
export_interval_millis=METRIC_EXPORT_INTERVAL_MS,
),
]
)
metrics.set_meter_provider(meter_provider)
SpannerMetricsTracerFactory()
except Exception as e:
log.warning(
"Failed to initialize Spanner built-in metrics. Error: %s", e
)
if not _metrics_monitor_initialized:
with _metrics_monitor_lock:
if not _metrics_monitor_initialized:
meter_provider = metrics.NoOpMeterProvider()
try:
if not _get_spanner_emulator_host():
meter_provider = MeterProvider(
metric_readers=[
PeriodicExportingMetricReader(
CloudMonitoringMetricsExporter(
project_id=project,
credentials=credentials,
),
export_interval_millis=METRIC_EXPORT_INTERVAL_MS,
),
]
)
metrics.set_meter_provider(meter_provider)
SpannerMetricsTracerFactory()
_metrics_monitor_initialized = True
except Exception as e:
log.warning(
"Failed to initialize Spanner built-in metrics. Error: %s",
e,
)
else:
SpannerMetricsTracerFactory(enabled=False)

Expand Down
88 changes: 78 additions & 10 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,28 +255,44 @@ def test_constructor_w_directed_read_options(self):
expected_scopes, creds, directed_read_options=self.DIRECTED_READ_OPTIONS
)

@mock.patch("google.cloud.spanner_v1.client.metrics")
@mock.patch("google.cloud.spanner_v1.client.CloudMonitoringMetricsExporter")
@mock.patch("google.cloud.spanner_v1.client.PeriodicExportingMetricReader")
@mock.patch("google.cloud.spanner_v1.client.MeterProvider")
@mock.patch("google.cloud.spanner_v1.client.SpannerMetricsTracerFactory")
@mock.patch.dict(os.environ, {"SPANNER_DISABLE_BUILTIN_METRICS": "false"})
def test_constructor_w_metrics_initialization_error(
self, mock_spanner_metrics_factory
self,
mock_spanner_metrics_factory,
mock_meter_provider,
mock_periodic_reader,
mock_exporter,
mock_metrics,
):
"""
Test that Client constructor handles exceptions during metrics
initialization and logs a warning.
"""
from google.cloud.spanner_v1.client import Client
from google.cloud.spanner_v1 import client as MUT

MUT._metrics_monitor_initialized = False
mock_spanner_metrics_factory.side_effect = Exception("Metrics init failed")
creds = build_scoped_credentials()

with self.assertLogs("google.cloud.spanner_v1.client", level="WARNING") as log:
client = Client(project=self.PROJECT, credentials=creds)
self.assertIsNotNone(client)
self.assertIn(
"Failed to initialize Spanner built-in metrics. Error: Metrics init failed",
log.output[0],
)
mock_spanner_metrics_factory.assert_called_once()
try:
with self.assertLogs(
"google.cloud.spanner_v1.client", level="WARNING"
) as log:
client = Client(project=self.PROJECT, credentials=creds)
self.assertIsNotNone(client)
self.assertIn(
"Failed to initialize Spanner built-in metrics. Error: Metrics init failed",
log.output[0],
)
mock_spanner_metrics_factory.assert_called_once()
mock_metrics.set_meter_provider.assert_called_once()
finally:
MUT._metrics_monitor_initialized = False

@mock.patch("google.cloud.spanner_v1.client.SpannerMetricsTracerFactory")
@mock.patch.dict(os.environ, {"SPANNER_DISABLE_BUILTIN_METRICS": "true"})
Expand All @@ -293,6 +309,58 @@ def test_constructor_w_disable_builtin_metrics_using_env(
self.assertIsNotNone(client)
mock_spanner_metrics_factory.assert_called_once_with(enabled=False)

@mock.patch("google.cloud.spanner_v1.client.metrics")
@mock.patch("google.cloud.spanner_v1.client.CloudMonitoringMetricsExporter")
@mock.patch("google.cloud.spanner_v1.client.PeriodicExportingMetricReader")
@mock.patch("google.cloud.spanner_v1.client.MeterProvider")
@mock.patch("google.cloud.spanner_v1.client.SpannerMetricsTracerFactory")
@mock.patch.dict(os.environ, {"SPANNER_DISABLE_BUILTIN_METRICS": "false"})
def test_constructor_metrics_singleton_behavior(
self,
mock_spanner_metrics_factory,
mock_meter_provider,
mock_periodic_reader,
mock_exporter,
mock_metrics,
):
"""
Test that metrics are only initialized once.
"""
from google.cloud.spanner_v1 import client as MUT

# Reset global state for this test
MUT._metrics_monitor_initialized = False
try:
creds = build_scoped_credentials()

# First client initialization
client1 = MUT.Client(project=self.PROJECT, credentials=creds)
self.assertIsNotNone(client1)
mock_metrics.set_meter_provider.assert_called_once()
mock_spanner_metrics_factory.assert_called_once()

# Verify MeterProvider chain was created
mock_meter_provider.assert_called_once()
mock_periodic_reader.assert_called_once()
mock_exporter.assert_called_once()

self.assertTrue(MUT._metrics_monitor_initialized)

# Reset mocks to verify they are NOT called again
mock_metrics.set_meter_provider.reset_mock()
mock_spanner_metrics_factory.reset_mock()
mock_meter_provider.reset_mock()

# Second client initialization
client2 = MUT.Client(project=self.PROJECT, credentials=creds)
self.assertIsNotNone(client2)
mock_metrics.set_meter_provider.assert_not_called()
mock_spanner_metrics_factory.assert_not_called()
mock_meter_provider.assert_not_called()
self.assertTrue(MUT._metrics_monitor_initialized)
finally:
MUT._metrics_monitor_initialized = False

@mock.patch("google.cloud.spanner_v1.client.SpannerMetricsTracerFactory")
def test_constructor_w_disable_builtin_metrics_using_option(
self, mock_spanner_metrics_factory
Expand Down
19 changes: 13 additions & 6 deletions tests/unit/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,24 @@ def patched_client(monkeypatch):
if SpannerMetricsTracerFactory._metrics_tracer_factory is not None:
SpannerMetricsTracerFactory._metrics_tracer_factory = None

client = Client(
project="test",
credentials=TestCredentials(),
# client_options={"api_endpoint": "none"}
)
yield client
# Reset the global flag to ensure metrics initialization runs
from google.cloud.spanner_v1 import client as client_module

client_module._metrics_monitor_initialized = False

with patch("google.cloud.spanner_v1.client.CloudMonitoringMetricsExporter"):
client = Client(
project="test",
credentials=TestCredentials(),
# client_options={"api_endpoint": "none"}
)
yield client

# Resetting
metrics.set_meter_provider(metrics.NoOpMeterProvider())
SpannerMetricsTracerFactory._metrics_tracer_factory = None
SpannerMetricsTracerFactory.current_metrics_tracer = None
client_module._metrics_monitor_initialized = False


def test_metrics_emission_with_failure_attempt(patched_client):
Expand Down
Loading