1414import pytest
1515import mock
1616
17+ from grpc import StatusCode
1718
18- from google .cloud .bigtable .data ._metrics .handlers .opentelemetry import _OpenTelemetryInstruments
19- from google .cloud .bigtable .data ._metrics .handlers .opentelemetry import OpenTelemetryMetricsHandler
19+ from google .cloud .bigtable .data ._metrics .data_model import (
20+ DEFAULT_CLUSTER_ID ,
21+ DEFAULT_ZONE ,
22+ ActiveOperationMetric ,
23+ CompletedAttemptMetric ,
24+ CompletedOperationMetric ,
25+ OperationType ,
26+ )
27+ from google .cloud .bigtable .data ._metrics .handlers .opentelemetry import (
28+ _OpenTelemetryInstruments ,
29+ OpenTelemetryMetricsHandler ,
30+ )
2031
21- class TestOpentelemetryInstruments :
2232
23- EXPPECTED_METRICS = [
33+ class TestOpentelemetryInstruments :
34+ EXPECTED_METRICS = [
2435 "operation_latencies" ,
2536 "first_response_latencies" ,
2637 "attempt_latencies" ,
2738 "server_latencies" ,
2839 "application_latencies" ,
2940 "throttling_latencies" ,
3041 "retry_count" ,
31- "connectivity_error_count"
42+ "connectivity_error_count" ,
3243 ]
3344
3445 def _make_one (self , meter_provider = None ):
@@ -40,60 +51,88 @@ def test_meter_name(self):
4051 self ._make_one (mock_meter_provider )
4152 mock_meter_provider .get_meter .assert_called_once_with (expected_name )
4253
43- @pytest .mark .parametrize ("metric_name" , [
44- m for m in EXPPECTED_METRICS if "latencies" in m
45- ] )
54+ @pytest .mark .parametrize (
55+ "metric_name" , [ m for m in EXPECTED_METRICS if "latencies" in m ]
56+ )
4657 def test_histogram_creation (self , metric_name ):
4758 mock_meter_provider = mock .Mock ()
4859 instruments = self ._make_one (mock_meter_provider )
4960 mock_meter = mock_meter_provider .get_meter ()
50- assert any ([call .kwargs ["name" ] == metric_name for call in mock_meter .create_histogram .call_args_list ])
51- assert all ([call .kwargs ["unit" ] == "ms" for call in mock_meter .create_histogram .call_args_list ])
52- assert all ([call .kwargs ["description" ] is not None for call in mock_meter .create_histogram .call_args_list ])
61+ assert any (
62+ [
63+ call .kwargs ["name" ] == metric_name
64+ for call in mock_meter .create_histogram .call_args_list
65+ ]
66+ )
67+ assert all (
68+ [
69+ call .kwargs ["unit" ] == "ms"
70+ for call in mock_meter .create_histogram .call_args_list
71+ ]
72+ )
73+ assert all (
74+ [
75+ call .kwargs ["description" ] is not None
76+ for call in mock_meter .create_histogram .call_args_list
77+ ]
78+ )
5379 assert getattr (instruments , metric_name ) is not None
5480
55- @pytest .mark .parametrize ("metric_name" , [
56- m for m in EXPPECTED_METRICS if "count" in m
57- ])
81+ @pytest .mark .parametrize ("metric_name" , [m for m in EXPECTED_METRICS if "count" in m ])
5882 def test_counter_creation (self , metric_name ):
5983 mock_meter_provider = mock .Mock ()
6084 instruments = self ._make_one (mock_meter_provider )
6185 mock_meter = mock_meter_provider .get_meter ()
62- assert any ([call .kwargs ["name" ] == metric_name for call in mock_meter .create_counter .call_args_list ])
63- assert all ([call .kwargs ["description" ] is not None for call in mock_meter .create_histogram .call_args_list ])
86+ assert any (
87+ [
88+ call .kwargs ["name" ] == metric_name
89+ for call in mock_meter .create_counter .call_args_list
90+ ]
91+ )
92+ assert all (
93+ [
94+ call .kwargs ["description" ] is not None
95+ for call in mock_meter .create_histogram .call_args_list
96+ ]
97+ )
6498 assert getattr (instruments , metric_name ) is not None
6599
66100 def test_global_provider (self ):
67101 instruments = self ._make_one ()
68102 # wait to import otel until after creating instance
69103 import opentelemetry
70- for metric_name in self .EXPPECTED_METRICS :
104+
105+ for metric_name in self .EXPECTED_METRICS :
71106 metric = getattr (instruments , metric_name )
72107 assert metric is not None
73108 if "latencies" in metric_name :
74109 assert isinstance (metric , opentelemetry .metrics .Histogram )
75110 else :
76111 assert isinstance (metric , opentelemetry .metrics .Counter )
77112
78- class TestOpentelemetryMetricsHandler :
79113
114+ class TestOpentelemetryMetricsHandler :
80115 def _make_one (self , ** kwargs ):
81116 return OpenTelemetryMetricsHandler (** kwargs )
82117
83118 def test_ctor_defaults (self ):
84119 from google .cloud .bigtable import __version__ as CLIENT_VERSION
120+
85121 expected_instance = "my_instance"
86122 expected_table = "my_table"
87- with mock .patch .object (OpenTelemetryMetricsHandler , "_generate_client_uid" ) as uid_mock :
123+ with mock .patch .object (
124+ OpenTelemetryMetricsHandler , "_generate_client_uid"
125+ ) as uid_mock :
88126 handler = self ._make_one (
89- instance_id = expected_instance ,
90- table_id = expected_table
127+ instance_id = expected_instance , table_id = expected_table
91128 )
92129 assert isinstance (handler .otel , _OpenTelemetryInstruments )
93130 assert handler .shared_labels ["resource_instance" ] == expected_instance
94131 assert handler .shared_labels ["resource_table" ] == expected_table
95132 assert handler .shared_labels ["app_profile" ] == "default"
96- assert handler .shared_labels ["client_name" ] == f"python-bigtable/{ CLIENT_VERSION } "
133+ assert (
134+ handler .shared_labels ["client_name" ] == f"python-bigtable/{ CLIENT_VERSION } "
135+ )
97136 assert handler .shared_labels ["client_uid" ] == uid_mock ()
98137
99138 def test_ctor_explicit (self ):
@@ -115,7 +154,9 @@ def test_ctor_explicit(self):
115154 assert handler .shared_labels ["resource_instance" ] == expected_instance
116155 assert handler .shared_labels ["resource_table" ] == expected_table
117156 assert handler .shared_labels ["app_profile" ] == expected_app_profile
118- assert handler .shared_labels ["client_name" ] == f"python-bigtable/{ expected_version } "
157+ assert (
158+ handler .shared_labels ["client_name" ] == f"python-bigtable/{ expected_version } "
159+ )
119160 assert handler .shared_labels ["client_uid" ] == expected_uid
120161
121162 @mock .patch ("socket.gethostname" , return_value = "hostname" )
@@ -128,19 +169,175 @@ def test_generate_client_uid_mock(self, socket_mock, os_mock, uuid_mock):
128169 @mock .patch ("socket.gethostname" , side_effect = [ValueError ("fail" )])
129170 @mock .patch ("os.getpid" , side_effect = [ValueError ("fail" )])
130171 @mock .patch ("uuid.uuid4" , return_value = "uid" )
131- def test_generate_client_uid_mock_with_exceptions (self , socket_mock , os_mock , uuid_mock ):
172+ def test_generate_client_uid_mock_with_exceptions (
173+ self , socket_mock , os_mock , uuid_mock
174+ ):
132175 uid = OpenTelemetryMetricsHandler ._generate_client_uid ()
133176 assert uid == "python-uid-@localhost"
134177
135178 def test_generate_client_uid (self ):
136179 import re
180+
137181 uid = OpenTelemetryMetricsHandler ._generate_client_uid ()
138182 # The expected pattern is python-<uuid>-<pid>@<hostname>
139- expected_pattern = r"python-[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}-\d+@.+"
183+ expected_pattern = (
184+ r"python-[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}-\d+@.+"
185+ )
140186 assert re .match (expected_pattern , uid )
141187
142- def test_on_operation_complete (self ):
143- pass
188+ @pytest .mark .parametrize (
189+ "op_type,is_streaming,first_response_latency_ns,attempts_count" ,
190+ [
191+ (OperationType .READ_ROWS , True , 12345 , 0 ),
192+ (OperationType .READ_ROWS , True , None , 2 ),
193+ (OperationType .MUTATE_ROW , False , None , 3 ),
194+ (
195+ OperationType .SAMPLE_ROW_KEYS ,
196+ False ,
197+ 12345 ,
198+ 1 ,
199+ ), # first_response_latency should be ignored
200+ ],
201+ )
202+ def test_on_operation_complete (
203+ self , op_type , is_streaming , first_response_latency_ns , attempts_count
204+ ):
205+ mock_instruments = mock .Mock (
206+ operation_latencies = mock .Mock (),
207+ first_response_latencies = mock .Mock (),
208+ retry_count = mock .Mock (),
209+ )
210+ handler = self ._make_one (
211+ instance_id = "inst" , table_id = "table" , instruments = mock_instruments
212+ )
213+ attempts = [mock .Mock () for _ in range (attempts_count )]
214+ op = CompletedOperationMetric (
215+ op_type = op_type ,
216+ uuid = "test-uuid" ,
217+ duration_ns = 1234567 ,
218+ completed_attempts = attempts ,
219+ final_status = StatusCode .OK ,
220+ cluster_id = "cluster" ,
221+ zone = "zone" ,
222+ is_streaming = is_streaming ,
223+ first_response_latency_ns = first_response_latency_ns ,
224+ )
225+
226+ handler .on_operation_complete (op )
144227
145- def test_on_attempt_complete (self ):
146- pass
228+ expected_labels = {
229+ "method" : op .op_type .value ,
230+ "status" : op .final_status .name ,
231+ "resource_zone" : op .zone ,
232+ "resource_cluster" : op .cluster_id ,
233+ ** handler .shared_labels ,
234+ }
235+
236+ # check operation_latencies
237+ mock_instruments .operation_latencies .record .assert_called_once_with (
238+ op .duration_ns / 1e6 ,
239+ {"streaming" : str (is_streaming ), ** expected_labels },
240+ )
241+
242+ # check first_response_latencies
243+ if (
244+ op_type == OperationType .READ_ROWS
245+ and first_response_latency_ns is not None
246+ ):
247+ mock_instruments .first_response_latencies .record .assert_called_once_with (
248+ first_response_latency_ns / 1e6 , expected_labels
249+ )
250+ else :
251+ mock_instruments .first_response_latencies .record .assert_not_called ()
252+
253+ # check retry_count
254+ if attempts :
255+ mock_instruments .retry_count .add .assert_called_once_with (
256+ len (attempts ) - 1 , expected_labels
257+ )
258+ else :
259+ mock_instruments .retry_count .add .assert_not_called ()
260+
261+ @pytest .mark .parametrize (
262+ "zone,cluster,gfe_latency_ns,is_first_attempt,flow_throttling_ns" ,
263+ [
264+ ("zone" , "cluster" , 12345 , True , 54321 ),
265+ (None , None , None , False , 0 ),
266+ ("zone" , "cluster" , 0 , True , 0 ), # gfe_latency_ns is 0
267+ ],
268+ )
269+ def test_on_attempt_complete (
270+ self , zone , cluster , gfe_latency_ns , is_first_attempt , flow_throttling_ns
271+ ):
272+ mock_instruments = mock .Mock (
273+ attempt_latencies = mock .Mock (),
274+ throttling_latencies = mock .Mock (),
275+ application_latencies = mock .Mock (),
276+ server_latencies = mock .Mock (),
277+ connectivity_error_count = mock .Mock (),
278+ )
279+ handler = self ._make_one (
280+ instance_id = "inst" , table_id = "table" , instruments = mock_instruments
281+ )
282+ attempt = CompletedAttemptMetric (
283+ duration_ns = 1234567 ,
284+ end_status = StatusCode .OK ,
285+ gfe_latency_ns = gfe_latency_ns ,
286+ application_blocking_time_ns = 234567 ,
287+ backoff_before_attempt_ns = 345678 ,
288+ grpc_throttling_time_ns = 456789 ,
289+ )
290+ op = ActiveOperationMetric (
291+ op_type = OperationType .READ_ROWS ,
292+ zone = zone ,
293+ cluster_id = cluster ,
294+ is_streaming = True ,
295+ flow_throttling_time_ns = flow_throttling_ns ,
296+ )
297+ if not is_first_attempt :
298+ op .completed_attempts .append (mock .Mock ())
299+
300+ handler .on_attempt_complete (attempt , op )
301+
302+ expected_labels = {
303+ "method" : op .op_type .value ,
304+ "resource_zone" : zone or DEFAULT_ZONE ,
305+ "resource_cluster" : cluster or DEFAULT_CLUSTER_ID ,
306+ ** handler .shared_labels ,
307+ }
308+ status = attempt .end_status .name
309+ is_streaming = str (op .is_streaming )
310+
311+ # check attempt_latencies
312+ mock_instruments .attempt_latencies .record .assert_called_once_with (
313+ attempt .duration_ns / 1e6 ,
314+ {"streaming" : is_streaming , "status" : status , ** expected_labels },
315+ )
316+
317+ # check throttling_latencies
318+ expected_throttling = attempt .grpc_throttling_time_ns / 1e6
319+ if is_first_attempt :
320+ expected_throttling += flow_throttling_ns / 1e6
321+ mock_instruments .throttling_latencies .record .assert_called_once_with (
322+ pytest .approx (expected_throttling ), expected_labels
323+ )
324+
325+ # check application_latencies
326+ mock_instruments .application_latencies .record .assert_called_once_with (
327+ (attempt .application_blocking_time_ns + attempt .backoff_before_attempt_ns )
328+ / 1e6 ,
329+ expected_labels ,
330+ )
331+
332+ # check server_latencies or connectivity_error_count
333+ if gfe_latency_ns is not None :
334+ mock_instruments .server_latencies .record .assert_called_once_with (
335+ gfe_latency_ns / 1e6 ,
336+ {"streaming" : is_streaming , "status" : status , ** expected_labels },
337+ )
338+ mock_instruments .connectivity_error_count .add .assert_not_called ()
339+ else :
340+ mock_instruments .server_latencies .record .assert_not_called ()
341+ mock_instruments .connectivity_error_count .add .assert_called_once_with (
342+ 1 , {"status" : status , ** expected_labels }
343+ )
0 commit comments