@@ -103,7 +103,10 @@ def test_get_otel_interceptor_sync_default(monkeypatch):
103103
104104 result = _observability ._get_otel_interceptor ()
105105 assert result is mock_interceptor
106- mock_otel_grpc .client_interceptor .assert_called_once_with (tracer_provider = None )
106+ mock_otel_grpc .client_interceptor .assert_called_once_with (
107+ tracer_provider = None ,
108+ request_hook = _observability ._client_request_hook ,
109+ )
107110
108111
109112def test_get_otel_interceptor_sync_config (monkeypatch ):
@@ -126,7 +129,8 @@ def test_get_otel_interceptor_sync_config(monkeypatch):
126129 result = _observability ._get_otel_interceptor (client_options = options )
127130 assert result is mock_interceptor
128131 mock_otel_grpc .client_interceptor .assert_called_once_with (
129- tracer_provider = mock_tracer_provider
132+ tracer_provider = mock_tracer_provider ,
133+ request_hook = _observability ._client_request_hook ,
130134 )
131135
132136
@@ -150,7 +154,8 @@ def test_get_otel_interceptor_sync_dict_config(monkeypatch):
150154 result = _observability ._get_otel_interceptor (client_options = options )
151155 assert result is mock_interceptor
152156 mock_otel_grpc .client_interceptor .assert_called_once_with (
153- tracer_provider = mock_tracer_provider
157+ tracer_provider = mock_tracer_provider ,
158+ request_hook = _observability ._client_request_hook ,
154159 )
155160
156161
@@ -174,7 +179,8 @@ def test_get_otel_interceptor_async(monkeypatch):
174179 result = _observability ._get_otel_interceptor (client_options = options , is_async = True )
175180 assert result is mock_async_interceptors
176181 mock_otel_grpc .aio_client_interceptors .assert_called_once_with (
177- tracer_provider = mock_tracer_provider
182+ tracer_provider = mock_tracer_provider ,
183+ request_hook = _observability ._client_request_hook ,
178184 )
179185
180186
@@ -216,7 +222,8 @@ def test_get_otel_interceptor_enabled(monkeypatch):
216222 assert callable (interceptor )
217223
218224 mock_otel_grpc .client_interceptor .assert_called_once_with (
219- tracer_provider = mock_tracer_provider
225+ tracer_provider = mock_tracer_provider ,
226+ request_hook = _observability ._client_request_hook ,
220227 )
221228
222229 result = interceptor (mock_raw_channel )
@@ -298,5 +305,47 @@ def test_get_otel_async_interceptor_enabled(monkeypatch):
298305 result = _observability .get_otel_async_interceptor (client_options = options )
299306 assert result is mock_async_interceptors
300307 mock_otel_grpc .aio_client_interceptors .assert_called_once_with (
301- tracer_provider = mock_tracer_provider
308+ tracer_provider = mock_tracer_provider ,
309+ request_hook = _observability ._client_request_hook ,
310+ )
311+
312+
313+ def test_extract_t4_attributes ():
314+ """Proves that _extract_t4_attributes correctly extracts GCP resource name,
315+ parent, and project ID from gRPC request objects.
316+ """
317+ assert _observability ._extract_t4_attributes (None ) == {}
318+
319+ # With name
320+ req_name = mock .Mock (spec = ["name" ], name = "req_name" )
321+ req_name .name = "projects/my-project/secrets/my-secret"
322+ attrs = _observability ._extract_t4_attributes (req_name )
323+ assert attrs ["gcp.resource.name" ] == "projects/my-project/secrets/my-secret"
324+ assert attrs ["gcp.project_id" ] == "my-project"
325+
326+ # With parent
327+ req_parent = mock .Mock (spec = ["parent" ], name = "req_parent" )
328+ req_parent .parent = "projects/parent-project"
329+ attrs = _observability ._extract_t4_attributes (req_parent )
330+ assert attrs ["gcp.resource.parent" ] == "projects/parent-project"
331+ assert attrs ["gcp.project_id" ] == "parent-project"
332+
333+
334+ def test_client_request_hook ():
335+ """Proves that _client_request_hook attaches extracted T4 attributes to recording spans."""
336+ # Non-recording span should not set attributes
337+ mock_span_non_rec = mock .Mock ()
338+ mock_span_non_rec .is_recording .return_value = False
339+ _observability ._client_request_hook (mock_span_non_rec , mock .Mock ())
340+ mock_span_non_rec .set_attribute .assert_not_called ()
341+
342+ # Recording span should set attributes
343+ mock_span_rec = mock .Mock ()
344+ mock_span_rec .is_recording .return_value = True
345+ req = mock .Mock (name = "req" )
346+ req .name = "projects/my-proj/secrets/s1"
347+ _observability ._client_request_hook (mock_span_rec , req )
348+ mock_span_rec .set_attribute .assert_any_call (
349+ "gcp.resource.name" , "projects/my-proj/secrets/s1"
302350 )
351+ mock_span_rec .set_attribute .assert_any_call ("gcp.project_id" , "my-proj" )
0 commit comments