Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 17ad4e0

Browse files
committed
gemini tests
1 parent f1ee799 commit 17ad4e0

1 file changed

Lines changed: 226 additions & 29 deletions

File tree

tests/unit/data/_metrics/test_opentelemetry_handler.py

Lines changed: 226 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,32 @@
1414
import pytest
1515
import 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

Comments
 (0)