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