1616
1717import asyncio
1818import logging
19+ import time
1920from types import TracebackType
2021from typing import TYPE_CHECKING
2122from typing import Any
2223from typing import Optional
24+ import uuid
2325
2426import google .auth
2527from google .auth .credentials import with_scopes_if_required
2931from google .cloud .alloydbconnector .enums import IPTypes
3032from google .cloud .alloydbconnector .enums import RefreshStrategy
3133from google .cloud .alloydbconnector .exceptions import ClosedConnectorError
34+ from google .cloud .alloydbconnector .instance import _parse_instance_uri
3235from google .cloud .alloydbconnector .instance import RefreshAheadCache
3336from 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 MetricRecorderType
41+ from google .cloud .alloydbconnector .telemetry import new_metric_recorder
42+ from google .cloud .alloydbconnector .telemetry import REFRESH_AHEAD_TYPE
43+ from google .cloud .alloydbconnector .telemetry import REFRESH_LAZY_TYPE
44+ from google .cloud .alloydbconnector .telemetry import TelemetryAttributes
3445from google .cloud .alloydbconnector .types import CacheTypes
3546from google .cloud .alloydbconnector .utils import generate_keys
3647from google .cloud .alloydbconnector .utils import strip_http_prefix
48+ from google .cloud .alloydbconnector .version import __version__
3749
3850if TYPE_CHECKING :
3951 from google .auth .credentials import Credentials
@@ -72,6 +84,12 @@ class AsyncConnector:
7284 of the following: RefreshStrategy.LAZY ("LAZY") or
7385 RefreshStrategy.BACKGROUND ("BACKGROUND").
7486 Default: RefreshStrategy.BACKGROUND
87+ enable_builtin_telemetry (bool): Enable built-in telemetry that
88+ reports connector metrics to the
89+ alloydb.googleapis.com/client/connector metric prefix in
90+ Cloud Monitoring. These metrics help AlloyDB improve performance
91+ and identify client connectivity problems. Set to False to
92+ disable. Default: True.
7593 """
7694
7795 def __init__ (
@@ -84,6 +102,7 @@ def __init__(
84102 ip_type : str | IPTypes = IPTypes .PRIVATE ,
85103 user_agent : Optional [str ] = None ,
86104 refresh_strategy : str | RefreshStrategy = RefreshStrategy .BACKGROUND ,
105+ enable_builtin_telemetry : bool = True ,
87106 ) -> None :
88107 self ._cache : dict [str , CacheTypes ] = {}
89108 # initialize default params
@@ -125,6 +144,44 @@ def __init__(
125144 self ._keys = None
126145 self ._client : Optional [AlloyDBClient ] = None
127146 self ._closed = False
147+ # built-in telemetry
148+ self ._enable_builtin_telemetry = enable_builtin_telemetry
149+ self ._client_uid = str (uuid .uuid4 ())
150+ self ._metric_recorders : dict [str , MetricRecorderType ] = {}
151+ self ._monitoring_client : Optional [object ] = None
152+ if self ._enable_builtin_telemetry :
153+ try :
154+ from google .cloud .monitoring_v3 import MetricServiceClient
155+
156+ self ._monitoring_client = MetricServiceClient (
157+ credentials = self ._credentials
158+ )
159+ except Exception as e :
160+ logger .debug (f"Built-in metrics exporter failed to initialize: { e } " )
161+
162+ def _metric_recorder (
163+ self ,
164+ instance_uri : str ,
165+ project : str ,
166+ region : str ,
167+ cluster : str ,
168+ name : str ,
169+ ) -> MetricRecorderType :
170+ """Get or lazily create a MetricRecorder for the given instance."""
171+ if instance_uri in self ._metric_recorders :
172+ return self ._metric_recorders [instance_uri ]
173+ mr = new_metric_recorder (
174+ enabled = self ._enable_builtin_telemetry ,
175+ project_id = project ,
176+ location = region ,
177+ cluster = cluster ,
178+ instance = name ,
179+ client_uid = self ._client_uid ,
180+ version = __version__ ,
181+ monitoring_client = self ._monitoring_client ,
182+ )
183+ self ._metric_recorders [instance_uri ] = mr
184+ return mr
128185
129186 async def connect (
130187 self ,
@@ -168,20 +225,36 @@ async def connect(
168225
169226 enable_iam_auth = kwargs .pop ("enable_iam_auth" , self ._enable_iam_auth )
170227
228+ # parse instance URI for telemetry resource labels
229+ project , region , cluster , name = _parse_instance_uri (instance_uri )
230+ mr = self ._metric_recorder (instance_uri , project , region , cluster , name )
231+
232+ attrs = TelemetryAttributes (
233+ iam_authn = enable_iam_auth ,
234+ refresh_type = (
235+ REFRESH_LAZY_TYPE
236+ if self ._refresh_strategy == RefreshStrategy .LAZY
237+ else REFRESH_AHEAD_TYPE
238+ ),
239+ )
240+ start_time = time .monotonic ()
241+
171242 # use existing connection info if possible
172- if instance_uri in self ._cache :
243+ cache_hit = instance_uri in self ._cache
244+ attrs .cache_hit = cache_hit
245+ if cache_hit :
173246 cache = self ._cache [instance_uri ]
174247 else :
175248 if self ._refresh_strategy == RefreshStrategy .LAZY :
176249 logger .debug (
177250 f"['{ instance_uri } ']: Refresh strategy is set to lazy refresh"
178251 )
179- cache = LazyRefreshCache (instance_uri , self ._client , self ._keys )
252+ cache = LazyRefreshCache (instance_uri , self ._client , self ._keys , mr )
180253 else :
181254 logger .debug (
182255 f"['{ instance_uri } ']: Refresh strategy is set to background refresh"
183256 )
184- cache = RefreshAheadCache (instance_uri , self ._client , self ._keys )
257+ cache = RefreshAheadCache (instance_uri , self ._client , self ._keys , mr )
185258 self ._cache [instance_uri ] = cache
186259 logger .debug (f"['{ instance_uri } ']: Connection info added to cache" )
187260
@@ -211,6 +284,8 @@ async def connect(
211284 except Exception :
212285 # with an error from AlloyDB API call or IP type, invalidate the
213286 # cache and re-raise the error
287+ attrs .dial_status = DIAL_CACHE_ERROR
288+ mr .record_dial_count (attrs )
214289 await self ._remove_cached (instance_uri )
215290 raise
216291 logger .debug (f"['{ instance_uri } ']: Connecting to { ip_address } :5433" )
@@ -228,14 +303,24 @@ async def get_authentication_token() -> str:
228303 if enable_iam_auth :
229304 kwargs ["password" ] = get_authentication_token
230305 try :
231- return await connector (
306+ conn = await connector (
232307 ip_address , await conn_info .create_ssl_context (), ** kwargs
233308 )
234309 except Exception :
235- # we attempt a force refresh, then throw the error
310+ # The Async connector doesn't distinguish between TCP, TLS, or MDX
311+ # errors. So treat all errors as TCP errors.
312+ attrs .dial_status = DIAL_TCP_ERROR
313+ mr .record_dial_count (attrs )
236314 await cache .force_refresh ()
237315 raise
238316
317+ # record successful dial metrics
318+ attrs .dial_status = DIAL_SUCCESS
319+ latency_ms = (time .monotonic () - start_time ) * 1000
320+ mr .record_dial_count (attrs )
321+ mr .record_dial_latency (latency_ms )
322+ return conn
323+
239324 async def _remove_cached (self , instance_uri : str ) -> None :
240325 """Stops all background refreshes and deletes the connection
241326 info cache from the map of caches.
@@ -262,4 +347,12 @@ async def close(self) -> None:
262347 """Helper function to cancel RefreshAheadCaches' tasks
263348 and close client."""
264349 await asyncio .gather (* [cache .close () for cache in self ._cache .values ()])
350+ # shut down metric recorders in executor to avoid blocking the
351+ # event loop (shutdown triggers a final gRPC export)
352+ loop = asyncio .get_event_loop ()
353+ for mr in self ._metric_recorders .values ():
354+ try :
355+ await loop .run_in_executor (None , mr .shutdown )
356+ except Exception :
357+ pass
265358 self ._closed = True
0 commit comments