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

Commit 88fe0b3

Browse files
committed
started otel unit tests
1 parent 65cd359 commit 88fe0b3

4 files changed

Lines changed: 181 additions & 4 deletions

File tree

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,8 @@ def __init__(
984984
exporter=client._gcp_metrics_exporter,
985985
instance_id=instance_id,
986986
table_id=table_id,
987-
app_profile_id=app_profile_id
987+
app_profile_id=app_profile_id,
988+
client_version=client._client_version(),
988989
)
989990
]
990991
)

google/cloud/bigtable/data/_metrics/handlers/opentelemetry.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,15 @@ def __init__(
124124
table_id: str,
125125
app_profile_id: str | None = None,
126126
client_uid: str | None = None,
127+
client_version: str | None = None,
127128
instruments: _OpenTelemetryInstruments = _OpenTelemetryInstruments(),
128-
**kwargs,
129129
):
130130
super().__init__()
131131
self.otel = instruments
132+
client_version = client_version or bigtable_version
132133
# fixed labels sent with each metric update
133134
self.shared_labels = {
134-
"client_name": f"python-bigtable/{bigtable_version}",
135+
"client_name": f"python-bigtable/{client_version}",
135136
"client_uid": client_uid or self._generate_client_uid(),
136137
"resource_instance": instance_id,
137138
"resource_table": table_id,

tests/unit/data/_async/test_client.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ def _make_client(cls, *args, use_emulator=True, **kwargs):
110110

111111
@CrossSync.pytest
112112
async def test_ctor(self):
113+
from google.cloud.bigtable.data._metrics.handlers.gcp_exporter import (
114+
BigtableMetricsExporter,
115+
)
116+
113117
expected_project = "project-id"
114118
expected_credentials = AnonymousCredentials()
115119
client = self._make_client(
@@ -123,6 +127,8 @@ async def test_ctor(self):
123127
assert client._channel_refresh_task is not None
124128
assert client.transport._credentials == expected_credentials
125129
assert isinstance(client._metrics_interceptor, CrossSync.MetricsInterceptor)
130+
assert client._gcp_metrics_exporter is not None
131+
assert isinstance(client._gcp_metrics_exporter, BigtableMetricsExporter)
126132
await client.close()
127133

128134
@CrossSync.pytest
@@ -189,6 +195,22 @@ async def test_ctor_dict_options(self):
189195
start_background_refresh.assert_called_once()
190196
await client.close()
191197

198+
@CrossSync.pytest
199+
async def test_metrics_exporter_init_shares_arguments(self):
200+
expected_credentials = AnonymousCredentials()
201+
expected_project = "custom_project"
202+
expected_options = client_options.ClientOptions()
203+
expected_options.credentials_file = None
204+
expected_options.quota_project_id = None
205+
with mock.patch("google.cloud.bigtable.data._metrics.handlers.gcp_exporter.BigtableMetricsExporter.__init__", return_value=None) as exporter_mock:
206+
async with self._make_client(project=expected_project, credentials=expected_credentials, client_options=expected_options):
207+
exporter_mock.assert_called_once_with(project_id=expected_project, credentials=expected_credentials, client_options=expected_options)
208+
209+
@CrossSync.pytest
210+
async def test_metrics_exporter_init_implicit_project(self):
211+
async with self._make_client() as client:
212+
assert client._gcp_metrics_exporter.project_id == client.project
213+
192214
@CrossSync.pytest
193215
async def test_veneer_grpc_headers(self):
194216
client_component = "data-async" if CrossSync.is_async else "data"
@@ -1163,6 +1185,9 @@ async def test_ctor(self):
11631185
from google.cloud.bigtable.data._metrics import (
11641186
BigtableClientSideMetricsController,
11651187
)
1188+
from google.cloud.bigtable.data._metrics import (
1189+
GoogleCloudMetricsHandler
1190+
)
11661191

11671192
expected_table_id = "table-id"
11681193
expected_instance_id = "instance-id"
@@ -1205,6 +1230,8 @@ async def test_ctor(self):
12051230
assert instance_key in client._active_instances
12061231
assert client._instance_owners[instance_key] == {id(table)}
12071232
assert isinstance(table._metrics, BigtableClientSideMetricsController)
1233+
assert len(table._metrics.handlers) == 1
1234+
assert isinstance(table._metrics.handlers[0], GoogleCloudMetricsHandler)
12081235
assert table.default_operation_timeout == expected_operation_timeout
12091236
assert table.default_attempt_timeout == expected_attempt_timeout
12101237
assert (
@@ -1495,7 +1522,7 @@ def _make_one(
14951522
async def test_ctor(self):
14961523
from google.cloud.bigtable.data._helpers import _WarmedInstanceKey
14971524
from google.cloud.bigtable.data._metrics import (
1498-
BigtableClientSideMetricsController,
1525+
BigtableClientSideMetricsController, GoogleCloudMetricsHandler
14991526
)
15001527

15011528
expected_table_id = "table-id"
@@ -1546,6 +1573,8 @@ async def test_ctor(self):
15461573
assert instance_key in client._active_instances
15471574
assert client._instance_owners[instance_key] == {id(view)}
15481575
assert isinstance(view._metrics, BigtableClientSideMetricsController)
1576+
assert len(view._metrics.handlers) == 1
1577+
assert isinstance(view._metrics.handlers[0], GoogleCloudMetricsHandler)
15491578
assert view.default_operation_timeout == expected_operation_timeout
15501579
assert view.default_attempt_timeout == expected_attempt_timeout
15511580
assert (
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Copyright 2024 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import pytest
15+
import mock
16+
17+
18+
from google.cloud.bigtable.data._metrics.handlers.opentelemetry import _OpenTelemetryInstruments
19+
from google.cloud.bigtable.data._metrics.handlers.opentelemetry import OpenTelemetryMetricsHandler
20+
21+
class TestOpentelemetryInstruments:
22+
23+
EXPPECTED_METRICS = [
24+
"operation_latencies",
25+
"first_response_latencies",
26+
"attempt_latencies",
27+
"server_latencies",
28+
"application_latencies",
29+
"throttling_latencies",
30+
"retry_count",
31+
"connectivity_error_count"
32+
]
33+
34+
def _make_one(self, meter_provider=None):
35+
return _OpenTelemetryInstruments(meter_provider)
36+
37+
def test_meter_name(self):
38+
expected_name = "bigtable.googleapis.com"
39+
mock_meter_provider = mock.Mock()
40+
self._make_one(mock_meter_provider)
41+
mock_meter_provider.get_meter.assert_called_once_with(expected_name)
42+
43+
@pytest.mark.parametrize("metric_name", [
44+
m for m in EXPPECTED_METRICS if "latencies" in m
45+
])
46+
def test_histogram_creation(self, metric_name):
47+
mock_meter_provider = mock.Mock()
48+
instruments = self._make_one(mock_meter_provider)
49+
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])
53+
assert getattr(instruments, metric_name) is not None
54+
55+
@pytest.mark.parametrize("metric_name", [
56+
m for m in EXPPECTED_METRICS if "count" in m
57+
])
58+
def test_counter_creation(self, metric_name):
59+
mock_meter_provider = mock.Mock()
60+
instruments = self._make_one(mock_meter_provider)
61+
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])
64+
assert getattr(instruments, metric_name) is not None
65+
66+
def test_global_provider(self):
67+
instruments = self._make_one()
68+
# wait to import otel until after creating instance
69+
import opentelemetry
70+
for metric_name in self.EXPPECTED_METRICS:
71+
metric = getattr(instruments, metric_name)
72+
assert metric is not None
73+
if "latencies" in metric_name:
74+
assert isinstance(metric, opentelemetry.metrics.Histogram)
75+
else:
76+
assert isinstance(metric, opentelemetry.metrics.Counter)
77+
78+
class TestOpentelemetryMetricsHandler:
79+
80+
def _make_one(self, **kwargs):
81+
return OpenTelemetryMetricsHandler(**kwargs)
82+
83+
def test_ctor_defaults(self):
84+
from google.cloud.bigtable import __version__ as CLIENT_VERSION
85+
expected_instance = "my_instance"
86+
expected_table = "my_table"
87+
with mock.patch.object(OpenTelemetryMetricsHandler, "_generate_client_uid") as uid_mock:
88+
handler = self._make_one(
89+
instance_id=expected_instance,
90+
table_id=expected_table
91+
)
92+
assert isinstance(handler.otel, _OpenTelemetryInstruments)
93+
assert handler.shared_labels["resource_instance"] == expected_instance
94+
assert handler.shared_labels["resource_table"] == expected_table
95+
assert handler.shared_labels["app_profile"] == "default"
96+
assert handler.shared_labels["client_name"] == f"python-bigtable/{CLIENT_VERSION}"
97+
assert handler.shared_labels["client_uid"] == uid_mock()
98+
99+
def test_ctor_explicit(self):
100+
expected_instance = "my_instance"
101+
expected_table = "my_table"
102+
expected_version = "my_version"
103+
expected_uid = "my_uid"
104+
expected_app_profile = "my_profile"
105+
expected_instruments = object()
106+
handler = self._make_one(
107+
instance_id=expected_instance,
108+
table_id=expected_table,
109+
app_profile_id=expected_app_profile,
110+
client_uid=expected_uid,
111+
client_version=expected_version,
112+
instruments=expected_instruments,
113+
)
114+
assert handler.otel == expected_instruments
115+
assert handler.shared_labels["resource_instance"] == expected_instance
116+
assert handler.shared_labels["resource_table"] == expected_table
117+
assert handler.shared_labels["app_profile"] == expected_app_profile
118+
assert handler.shared_labels["client_name"] == f"python-bigtable/{expected_version}"
119+
assert handler.shared_labels["client_uid"] == expected_uid
120+
121+
@mock.patch("socket.gethostname", return_value="hostname")
122+
@mock.patch("os.getpid", return_value="pid")
123+
@mock.patch("uuid.uuid4", return_value="uid")
124+
def test_generate_client_uid_mock(self, socket_mock, os_mock, uuid_mock):
125+
uid = OpenTelemetryMetricsHandler._generate_client_uid()
126+
assert uid == "python-uid-pid@hostname"
127+
128+
@mock.patch("socket.gethostname", side_effect=[ValueError("fail")])
129+
@mock.patch("os.getpid", side_effect=[ValueError("fail")])
130+
@mock.patch("uuid.uuid4", return_value="uid")
131+
def test_generate_client_uid_mock_with_exceptions(self, socket_mock, os_mock, uuid_mock):
132+
uid = OpenTelemetryMetricsHandler._generate_client_uid()
133+
assert uid == "python-uid-@localhost"
134+
135+
def test_generate_client_uid(self):
136+
import re
137+
uid = OpenTelemetryMetricsHandler._generate_client_uid()
138+
# 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+@.+"
140+
assert re.match(expected_pattern, uid)
141+
142+
def test_on_operation_complete(self):
143+
pass
144+
145+
def test_on_attempt_complete(self):
146+
pass

0 commit comments

Comments
 (0)