Skip to content

Commit 22577a0

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 eba5aaa commit 22577a0

9 files changed

Lines changed: 879 additions & 24 deletions

File tree

google/cloud/alloydbconnector/async_connector.py

Lines changed: 98 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
@@ -29,11 +31,21 @@
2931
from google.cloud.alloydbconnector.enums import IPTypes
3032
from google.cloud.alloydbconnector.enums import RefreshStrategy
3133
from google.cloud.alloydbconnector.exceptions import ClosedConnectorError
34+
from google.cloud.alloydbconnector.instance import _parse_instance_uri
3235
from google.cloud.alloydbconnector.instance import RefreshAheadCache
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 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
3445
from google.cloud.alloydbconnector.types import CacheTypes
3546
from google.cloud.alloydbconnector.utils import generate_keys
3647
from google.cloud.alloydbconnector.utils import strip_http_prefix
48+
from google.cloud.alloydbconnector.version import __version__
3749

3850
if 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

Comments
 (0)