1313# limitations under the License.
1414from __future__ import annotations
1515
16+ from typing import Sequence
17+
18+ import time
19+ from functools import wraps
20+
21+ from google .cloud .bigtable .data ._metrics .data_model import ActiveOperationMetric
22+ from google .cloud .bigtable .data ._metrics .data_model import OperationState
23+ from google .cloud .bigtable .data ._metrics .data_model import OperationType
24+
1625from google .cloud .bigtable .data ._cross_sync import CrossSync
1726
1827if CrossSync .is_async :
1928 from grpc .aio import UnaryUnaryClientInterceptor
2029 from grpc .aio import UnaryStreamClientInterceptor
30+ from grpc .aio import AioRpcError
2131else :
2232 from grpc import UnaryUnaryClientInterceptor
2333 from grpc import UnaryStreamClientInterceptor
2636__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.metrics_interceptor"
2737
2838
39+ def _with_active_operation (func ):
40+ """
41+ Decorator for interceptor methods to extract the active operation associated with the
42+ in-scope contextvars, and pass it to the decorated function.
43+ """
44+
45+ @wraps (func )
46+ def wrapper (self , continuation , client_call_details , request ):
47+ operation : ActiveOperationMetric | None = ActiveOperationMetric .from_context ()
48+
49+ if operation :
50+ # start a new attempt if not started
51+ if (
52+ operation .state == OperationState .CREATED
53+ or operation .state == OperationState .BETWEEN_ATTEMPTS
54+ ):
55+ operation .start_attempt ()
56+ # wrap continuation in logic to process the operation
57+ return func (self , operation , continuation , client_call_details , request )
58+ else :
59+ # if operation not found, return unwrapped continuation
60+ return continuation (client_call_details , request )
61+
62+ return wrapper
63+
64+
65+ @CrossSync .convert
66+ async def _get_metadata (source ) -> dict [str , str | bytes ] | None :
67+ """Helper to extract metadata from a call or RpcError"""
68+ try :
69+ metadata : Sequence [tuple [str , str | bytes ]]
70+ if CrossSync .is_async :
71+ # grpc.aio returns metadata in Metadata objects
72+ if isinstance (source , AioRpcError ):
73+ metadata = list (source .trailing_metadata ()) + list (
74+ source .initial_metadata ()
75+ )
76+ else :
77+ metadata = list (await source .trailing_metadata ()) + list (
78+ await source .initial_metadata ()
79+ )
80+ else :
81+ # sync grpc returns metadata as a sequence of tuples
82+ metadata = source .trailing_metadata () + source .initial_metadata ()
83+ # convert metadata to dict format
84+ return {k : v for (k , v ) in metadata }
85+ except Exception :
86+ # ignore errors while fetching metadata
87+ return None
88+
89+
2990@CrossSync .convert_class (sync_name = "BigtableMetricsInterceptor" )
3091class AsyncBigtableMetricsInterceptor (
3192 UnaryUnaryClientInterceptor , UnaryStreamClientInterceptor
@@ -35,21 +96,33 @@ class AsyncBigtableMetricsInterceptor(
3596 """
3697
3798 @CrossSync .convert
38- async def intercept_unary_unary (self , continuation , client_call_details , request ):
99+ @_with_active_operation
100+ async def intercept_unary_unary (
101+ self , operation , continuation , client_call_details , request
102+ ):
39103 """
40104 Interceptor for unary rpcs:
41105 - MutateRow
42106 - CheckAndMutateRow
43107 - ReadModifyWriteRow
44108 """
109+ metadata = None
45110 try :
46111 call = await continuation (client_call_details , request )
112+ metadata = await _get_metadata (call )
47113 return call
48114 except Exception as rpc_error :
115+ metadata = await _get_metadata (rpc_error )
49116 raise rpc_error
117+ finally :
118+ if metadata is not None :
119+ operation .add_response_metadata (metadata )
50120
51121 @CrossSync .convert
52- async def intercept_unary_stream (self , continuation , client_call_details , request ):
122+ @_with_active_operation
123+ async def intercept_unary_stream (
124+ self , operation , continuation , client_call_details , request
125+ ):
53126 """
54127 Interceptor for streaming rpcs:
55128 - ReadRows
@@ -58,21 +131,42 @@ async def intercept_unary_stream(self, continuation, client_call_details, reques
58131 """
59132 try :
60133 return self ._streaming_generator_wrapper (
61- await continuation (client_call_details , request )
134+ operation , await continuation (client_call_details , request )
62135 )
63136 except Exception as rpc_error :
64137 # handle errors while intializing stream
138+ metadata = await _get_metadata (rpc_error )
139+ if metadata is not None :
140+ operation .add_response_metadata (metadata )
65141 raise rpc_error
66142
67143 @staticmethod
68144 @CrossSync .convert
69- async def _streaming_generator_wrapper (call ):
145+ async def _streaming_generator_wrapper (operation , call ):
70146 """
71147 Wrapped generator to be returned by intercept_unary_stream.
72148 """
149+ # only track has_first response for READ_ROWS
150+ has_first_response = (
151+ operation .first_response_latency_ns is not None
152+ or operation .op_type != OperationType .READ_ROWS
153+ )
154+ encountered_exc = None
73155 try :
74156 async for response in call :
157+ # record time to first response. Currently only used for READ_ROWs
158+ if not has_first_response :
159+ operation .first_response_latency_ns = (
160+ time .monotonic_ns () - operation .start_time_ns
161+ )
162+ has_first_response = True
75163 yield response
76164 except Exception as e :
77165 # handle errors while processing stream
78- raise e
166+ encountered_exc = e
167+ raise
168+ finally :
169+ if call is not None :
170+ metadata = await _get_metadata (encountered_exc or call )
171+ if metadata is not None :
172+ operation .add_response_metadata (metadata )
0 commit comments