Skip to content

Commit 43b786c

Browse files
feat(bigtable): client side metrics handlers (#16760)
Migrate googleapis/python-bigtable#1189 to the monorepo This PR builds off of googleapis/python-bigtable#1187 to add handlers to the client-side metrics system, which can subscribe to the metrics stream, and export the results into different collection systems We add two handlers to the system: - `GoogleCloudMetricsHandler`: sends metrics to a private OpenTelemetry meter, and then periodically exports them to GCP. Built on top of `OpenTelemetryMetricsHandler` - `OpenTelemetryMetricsHandler`: sends metrics to the root MeterProvider, so the user can access the exported metrics for their own systems. This will be off by default, but can be added alongside `GoogleCloudMetricsHandler` if needed --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent b84b754 commit 43b786c

21 files changed

Lines changed: 2036 additions & 111 deletions

packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/client.py

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import abc
1919
import concurrent.futures
20+
import logging
2021
import os
2122
import random
2223
import time
@@ -64,10 +65,16 @@
6465
_WarmedInstanceKey,
6566
)
6667
from google.cloud.bigtable.data._metrics import (
68+
ActiveOperationMetric,
6769
BigtableClientSideMetricsController,
6870
OperationType,
6971
tracked_retry,
7072
)
73+
from google.cloud.bigtable.data._metrics.handlers._base import MetricsHandler
74+
from google.cloud.bigtable.data._metrics.handlers.gcp_exporter import (
75+
BigtableMetricsExporter,
76+
GoogleCloudMetricsHandler,
77+
)
7178
from google.cloud.bigtable.data.exceptions import (
7279
FailedQueryShardError,
7380
ShardedReadRowsExceptionGroup,
@@ -159,6 +166,8 @@
159166

160167
__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.client"
161168

169+
_LOGGER = logging.getLogger(__name__)
170+
162171

163172
@CrossSync.convert_class(
164173
sync_name="BigtableDataClient",
@@ -262,6 +271,27 @@ def __init__(
262271
"is the default."
263272
)
264273
self._is_closed = CrossSync.Event()
274+
handlers: list[MetricsHandler] = []
275+
if self._emulator_host is None:
276+
try:
277+
# create a metrics exporter using the same client configuration
278+
exporter = BigtableMetricsExporter(
279+
credentials=self._credentials,
280+
client_options=client_options,
281+
)
282+
handlers.append(
283+
GoogleCloudMetricsHandler(
284+
exporter=exporter,
285+
client_version=self._client_version(),
286+
)
287+
)
288+
except Exception as e:
289+
_LOGGER.warning(
290+
"Failed to initialize Google Cloud Metrics Exporter: %s. "
291+
"Client-side metrics will be disabled.",
292+
e,
293+
)
294+
self._metrics = BigtableClientSideMetricsController(handlers=handlers)
265295
self.transport = cast(TransportType, self._gapic_client.transport)
266296
# keep track of active instances to for warmup on channel refresh
267297
self._active_instances: Set[_WarmedInstanceKey] = set()
@@ -394,6 +424,7 @@ async def close(self, timeout: float | None = 2.0):
394424
if self._executor:
395425
self._executor.shutdown(wait=False)
396426
self._channel_refresh_task = None
427+
self._metrics.close()
397428

398429
@CrossSync.convert
399430
async def _ping_and_warm_instances(
@@ -1109,8 +1140,6 @@ def __init__(
11091140
default_retryable_errors or ()
11101141
)
11111142

1112-
self._metrics = BigtableClientSideMetricsController()
1113-
11141143
try:
11151144
self._register_instance_future = CrossSync.create_task(
11161145
self.client._register_instance,
@@ -1124,6 +1153,21 @@ def __init__(
11241153
f"{self.__class__.__name__} must be created within an async event loop context."
11251154
) from e
11261155

1156+
def _create_operation(
1157+
self, op_type: OperationType, **kwargs
1158+
) -> ActiveOperationMetric:
1159+
table_id = getattr(self, "table_id", None) or getattr(
1160+
self, "materialized_view_id", None
1161+
)
1162+
return self.client._metrics.create_operation(
1163+
op_type,
1164+
project_id=self.client.project,
1165+
instance_id=self.instance_id,
1166+
table_id=table_id,
1167+
app_profile_id=self.app_profile_id,
1168+
**kwargs,
1169+
)
1170+
11271171
@property
11281172
@abc.abstractmethod
11291173
def _request_path(self) -> dict[str, str]:
@@ -1189,9 +1233,7 @@ async def read_rows_stream(
11891233
self,
11901234
operation_timeout=operation_timeout,
11911235
attempt_timeout=attempt_timeout,
1192-
metric=self._metrics.create_operation(
1193-
OperationType.READ_ROWS, is_streaming=True
1194-
),
1236+
metric=self._create_operation(OperationType.READ_ROWS, is_streaming=True),
11951237
retryable_exceptions=retryable_excs,
11961238
)
11971239
return row_merger.start_operation()
@@ -1295,9 +1337,7 @@ async def read_row(
12951337
self,
12961338
operation_timeout=operation_timeout,
12971339
attempt_timeout=attempt_timeout,
1298-
metric=self._metrics.create_operation(
1299-
OperationType.READ_ROWS, is_streaming=False
1300-
),
1340+
metric=self._create_operation(OperationType.READ_ROWS, is_streaming=False),
13011341
retryable_exceptions=retryable_excs,
13021342
)
13031343
results_generator = row_merger.start_operation()
@@ -1512,9 +1552,7 @@ async def sample_row_keys(
15121552
retryable_excs = _get_retryable_errors(retryable_errors, self)
15131553
predicate = retries.if_exception_type(*retryable_excs)
15141554

1515-
with self._metrics.create_operation(
1516-
OperationType.SAMPLE_ROW_KEYS
1517-
) as operation_metric:
1555+
with self._create_operation(OperationType.SAMPLE_ROW_KEYS) as operation_metric:
15181556

15191557
@CrossSync.convert
15201558
async def execute_rpc():
@@ -1646,9 +1684,7 @@ async def mutate_row(
16461684
# mutations should not be retried
16471685
predicate = retries.if_exception_type()
16481686

1649-
with self._metrics.create_operation(
1650-
OperationType.MUTATE_ROW
1651-
) as operation_metric:
1687+
with self._create_operation(OperationType.MUTATE_ROW) as operation_metric:
16521688
target = partial(
16531689
self.client._gapic_client.mutate_row,
16541690
request=MutateRowRequest(
@@ -1722,7 +1758,7 @@ async def bulk_mutate_rows(
17221758
mutation_entries,
17231759
operation_timeout,
17241760
attempt_timeout,
1725-
metric=self._metrics.create_operation(OperationType.BULK_MUTATE_ROWS),
1761+
metric=self._create_operation(OperationType.BULK_MUTATE_ROWS),
17261762
retryable_exceptions=retryable_excs,
17271763
)
17281764
await operation.start()
@@ -1781,7 +1817,7 @@ async def check_and_mutate_row(
17811817
false_case_mutations = [false_case_mutations]
17821818
false_case_list = [m._to_pb() for m in false_case_mutations or []]
17831819

1784-
with self._metrics.create_operation(OperationType.CHECK_AND_MUTATE):
1820+
with self._create_operation(OperationType.CHECK_AND_MUTATE):
17851821
result = await self.client._gapic_client.check_and_mutate_row(
17861822
request=CheckAndMutateRowRequest(
17871823
true_mutations=true_case_list,
@@ -1839,7 +1875,7 @@ async def read_modify_write_row(
18391875
if not rules:
18401876
raise ValueError("rules must contain at least one item")
18411877

1842-
with self._metrics.create_operation(OperationType.READ_MODIFY_WRITE):
1878+
with self._create_operation(OperationType.READ_MODIFY_WRITE):
18431879
result = await self.client._gapic_client.read_modify_write_row(
18441880
request=ReadModifyWriteRowRequest(
18451881
rules=[rule._to_pb() for rule in rules],
@@ -1860,7 +1896,6 @@ async def close(self):
18601896
"""
18611897
Called to close the Table instance and release any resources held by it.
18621898
"""
1863-
self._metrics.close()
18641899
if self._register_instance_future:
18651900
self._register_instance_future.cancel()
18661901
self.client._remove_instance_registration(

packages/google-cloud-bigtable/google/cloud/bigtable/data/_async/mutations_batcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ async def _flush_internal(self, new_entries: list[RowMutationEntry]):
379379
# flush new entries
380380
in_process_requests: list[CrossSync.Future[list[FailedMutationEntryError]]] = []
381381
async for batch, metric in self._flow_control.add_to_flow_with_metrics(
382-
new_entries, self._target._metrics
382+
new_entries, self._target.client._metrics
383383
):
384384
batch_task = CrossSync.create_task(
385385
self._execute_mutate_rows,

packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@
1919
OperationState,
2020
OperationType,
2121
)
22+
from google.cloud.bigtable.data._metrics.handlers.gcp_exporter import (
23+
GoogleCloudMetricsHandler,
24+
)
25+
from google.cloud.bigtable.data._metrics.handlers.opentelemetry import (
26+
OpenTelemetryMetricsHandler,
27+
)
2228
from google.cloud.bigtable.data._metrics.metrics_controller import (
2329
BigtableClientSideMetricsController,
2430
)
2531
from google.cloud.bigtable.data._metrics.tracked_retry import tracked_retry
2632

2733
__all__ = (
2834
"BigtableClientSideMetricsController",
35+
"OpenTelemetryMetricsHandler",
36+
"GoogleCloudMetricsHandler",
2937
"OperationType",
3038
"OperationState",
3139
"ActiveOperationMetric",

packages/google-cloud-bigtable/google/cloud/bigtable/data/_metrics/data_model.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ class CompletedOperationMetric:
118118
cluster_id: str
119119
zone: str
120120
is_streaming: bool
121+
project_id: str | None = None
122+
instance_id: str | None = None
123+
table_id: str | None = None
124+
app_profile_id: str | None = None
121125
first_response_latency_ns: int | None = None
122126
flow_throttling_time_ns: int = 0
123127

@@ -160,6 +164,10 @@ class ActiveOperationMetric:
160164
active_attempt: ActiveAttemptMetric | None = None
161165
cluster_id: str | None = None
162166
zone: str | None = None
167+
project_id: str | None = None
168+
instance_id: str | None = None
169+
table_id: str | None = None
170+
app_profile_id: str | None = None
163171
completed_attempts: list[CompletedAttemptMetric] = field(default_factory=list)
164172
is_streaming: bool = False # only True for read_rows operations
165173
handlers: list[MetricsHandler] = field(default_factory=list)
@@ -375,6 +383,10 @@ def end_with_status(self, status: StatusCode | BaseException) -> None:
375383
cluster_id=self.cluster_id or DEFAULT_CLUSTER_ID,
376384
zone=self.zone or DEFAULT_ZONE,
377385
is_streaming=self.is_streaming,
386+
project_id=self.project_id,
387+
instance_id=self.instance_id,
388+
table_id=self.table_id,
389+
app_profile_id=self.app_profile_id,
378390
first_response_latency_ns=self.first_response_latency_ns,
379391
flow_throttling_time_ns=self.flow_throttling_time_ns,
380392
)

0 commit comments

Comments
 (0)