Skip to content

Commit 1a0f43a

Browse files
committed
feat: add support for disabling built-in metrics
This commit adds the OpenTelemetry-based wiring to report on internal operations to improve connectivity. To disable this internal metric collection, set enable_builtin_telemetry to False when creating a Connetor or AsyncConnector. Fixes #449
1 parent f94d500 commit 1a0f43a

10 files changed

Lines changed: 1185 additions & 22 deletions

File tree

google/cloud/alloydbconnector/async_connector.py

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
import asyncio
1818
import logging
19+
import time
1920
from types import TracebackType
2021
from typing import TYPE_CHECKING
2122
from typing import Any
2223
from typing import Optional
24+
import uuid
2325

2426
import google.auth
2527
from google.auth.credentials import with_scopes_if_required
@@ -30,10 +32,21 @@
3032
from google.cloud.alloydbconnector.enums import RefreshStrategy
3133
from google.cloud.alloydbconnector.exceptions import ClosedConnectorError
3234
from google.cloud.alloydbconnector.instance import RefreshAheadCache
35+
from google.cloud.alloydbconnector.instance import _parse_instance_uri
3336
from google.cloud.alloydbconnector.lazy import LazyRefreshCache
37+
from google.cloud.alloydbconnector.telemetry import DIAL_CACHE_ERROR
38+
from google.cloud.alloydbconnector.telemetry import DIAL_SUCCESS
39+
from google.cloud.alloydbconnector.telemetry import DIAL_TCP_ERROR
40+
from google.cloud.alloydbconnector.telemetry import REFRESH_AHEAD_TYPE
41+
from google.cloud.alloydbconnector.telemetry import REFRESH_LAZY_TYPE
42+
from google.cloud.alloydbconnector.telemetry import MetricRecorderType
43+
from google.cloud.alloydbconnector.telemetry import TelemetryAttributes
44+
from google.cloud.alloydbconnector.telemetry import TelemetryProviderType
45+
from google.cloud.alloydbconnector.telemetry import new_telemetry_provider
3446
from google.cloud.alloydbconnector.types import CacheTypes
3547
from google.cloud.alloydbconnector.utils import generate_keys
3648
from google.cloud.alloydbconnector.utils import strip_http_prefix
49+
from google.cloud.alloydbconnector.version import __version__
3750

3851
if TYPE_CHECKING:
3952
from google.auth.credentials import Credentials
@@ -72,6 +85,12 @@ class AsyncConnector:
7285
of the following: RefreshStrategy.LAZY ("LAZY") or
7386
RefreshStrategy.BACKGROUND ("BACKGROUND").
7487
Default: RefreshStrategy.BACKGROUND
88+
enable_builtin_telemetry (bool): Enable built-in telemetry that
89+
reports connector metrics to the
90+
alloydb.googleapis.com/client/connector metric prefix in
91+
Cloud Monitoring. These metrics help AlloyDB improve performance
92+
and identify client connectivity problems. Set to False to
93+
disable. Default: True.
7594
"""
7695

7796
def __init__(
@@ -84,6 +103,7 @@ def __init__(
84103
ip_type: str | IPTypes = IPTypes.PRIVATE,
85104
user_agent: Optional[str] = None,
86105
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
106+
enable_builtin_telemetry: bool = True,
87107
) -> None:
88108
self._cache: dict[str, CacheTypes] = {}
89109
# initialize default params
@@ -132,6 +152,49 @@ def __init__(
132152
pass
133153
self._client: Optional[AlloyDBClient] = None
134154
self._closed = False
155+
# built-in telemetry
156+
self._enable_builtin_telemetry = enable_builtin_telemetry
157+
self._client_uid = str(uuid.uuid4())
158+
self._metric_recorders: dict[str, MetricRecorderType] = {}
159+
self._telemetry_provider: Optional[TelemetryProviderType] = None
160+
self._monitoring_client: Optional[object] = None
161+
if self._enable_builtin_telemetry:
162+
try:
163+
from google.cloud.monitoring_v3 import MetricServiceClient
164+
165+
self._monitoring_client = MetricServiceClient(
166+
credentials=self._credentials
167+
)
168+
except Exception as e:
169+
logger.debug(f"Built-in metrics exporter failed to initialize: {e}")
170+
171+
def _get_telemetry_provider(self, project_id: str) -> TelemetryProviderType:
172+
"""Get or lazily create the TelemetryProvider on first connect."""
173+
if self._telemetry_provider is not None:
174+
return self._telemetry_provider
175+
self._telemetry_provider = new_telemetry_provider(
176+
enabled=self._enable_builtin_telemetry,
177+
project_id=project_id,
178+
client_uid=self._client_uid,
179+
version=__version__,
180+
monitoring_client=self._monitoring_client,
181+
)
182+
return self._telemetry_provider
183+
184+
def _metric_recorder(self, instance_uri: str) -> MetricRecorderType:
185+
"""Get or lazily create a MetricRecorder for the given instance."""
186+
if instance_uri in self._metric_recorders:
187+
return self._metric_recorders[instance_uri]
188+
project, region, cluster, name = _parse_instance_uri(instance_uri)
189+
provider = self._get_telemetry_provider(project)
190+
mr = provider.create_metric_recorder(
191+
project_id=project,
192+
location=region,
193+
cluster=cluster,
194+
instance=name,
195+
)
196+
self._metric_recorders[instance_uri] = mr
197+
return mr
135198

136199
async def connect(
137200
self,
@@ -175,20 +238,34 @@ async def connect(
175238

176239
enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
177240

241+
mr = self._metric_recorder(instance_uri)
242+
243+
attrs = TelemetryAttributes(
244+
iam_authn=enable_iam_auth,
245+
refresh_type=(
246+
REFRESH_LAZY_TYPE
247+
if self._refresh_strategy == RefreshStrategy.LAZY
248+
else REFRESH_AHEAD_TYPE
249+
),
250+
)
251+
start_time = time.monotonic()
252+
178253
# use existing connection info if possible
179-
if instance_uri in self._cache:
254+
cache_hit = instance_uri in self._cache
255+
attrs.cache_hit = cache_hit
256+
if cache_hit:
180257
cache = self._cache[instance_uri]
181258
else:
182259
if self._refresh_strategy == RefreshStrategy.LAZY:
183260
logger.debug(
184261
f"['{instance_uri}']: Refresh strategy is set to lazy refresh"
185262
)
186-
cache = LazyRefreshCache(instance_uri, self._client, self._keys)
263+
cache = LazyRefreshCache(instance_uri, self._client, self._keys, mr)
187264
else:
188265
logger.debug(
189266
f"['{instance_uri}']: Refresh strategy is set to background refresh"
190267
)
191-
cache = RefreshAheadCache(instance_uri, self._client, self._keys)
268+
cache = RefreshAheadCache(instance_uri, self._client, self._keys, mr)
192269
self._cache[instance_uri] = cache
193270
logger.debug(f"['{instance_uri}']: Connection info added to cache")
194271

@@ -218,6 +295,8 @@ async def connect(
218295
except Exception:
219296
# with an error from AlloyDB API call or IP type, invalidate the
220297
# cache and re-raise the error
298+
attrs.dial_status = DIAL_CACHE_ERROR
299+
mr.record_dial_count(attrs)
221300
await self._remove_cached(instance_uri)
222301
raise
223302
logger.debug(f"['{instance_uri}']: Connecting to {ip_address}:5433")
@@ -235,14 +314,24 @@ async def get_authentication_token() -> str:
235314
if enable_iam_auth:
236315
kwargs["password"] = get_authentication_token
237316
try:
238-
return await connector(
317+
conn = await connector(
239318
ip_address, await conn_info.create_ssl_context(), **kwargs
240319
)
241320
except Exception:
242-
# we attempt a force refresh, then throw the error
321+
# The Async connector doesn't distinguish between TCP, TLS, or MDX
322+
# errors. So treat all errors as TCP errors.
323+
attrs.dial_status = DIAL_TCP_ERROR
324+
mr.record_dial_count(attrs)
243325
await cache.force_refresh()
244326
raise
245327

328+
# record successful dial metrics
329+
attrs.dial_status = DIAL_SUCCESS
330+
latency_ms = (time.monotonic() - start_time) * 1000
331+
mr.record_dial_count(attrs)
332+
mr.record_dial_latency(latency_ms)
333+
return conn
334+
246335
async def _remove_cached(self, instance_uri: str) -> None:
247336
"""Stops all background refreshes and deletes the connection
248337
info cache from the map of caches.
@@ -269,4 +358,12 @@ async def close(self) -> None:
269358
"""Helper function to cancel RefreshAheadCaches' tasks
270359
and close client."""
271360
await asyncio.gather(*[cache.close() for cache in self._cache.values()])
361+
# shut down telemetry provider in executor to avoid blocking the
362+
# event loop (shutdown triggers a final gRPC export)
363+
if self._telemetry_provider is not None:
364+
loop = asyncio.get_event_loop()
365+
try:
366+
await loop.run_in_executor(None, self._telemetry_provider.shutdown)
367+
except Exception:
368+
pass
272369
self._closed = True

0 commit comments

Comments
 (0)