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
3032from google .cloud .alloydbconnector .enums import RefreshStrategy
3133from google .cloud .alloydbconnector .exceptions import ClosedConnectorError
3234from google .cloud .alloydbconnector .instance import RefreshAheadCache
35+ from google .cloud .alloydbconnector .instance import _parse_instance_uri
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 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
3446from google .cloud .alloydbconnector .types import CacheTypes
3547from google .cloud .alloydbconnector .utils import generate_keys
3648from google .cloud .alloydbconnector .utils import strip_http_prefix
49+ from google .cloud .alloydbconnector .version import __version__
3750
3851if 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