Skip to content

Commit d6307d6

Browse files
committed
fix(observability): ensure response hook only records OK on successful calls
1 parent c68dab6 commit d6307d6

2 files changed

Lines changed: 41 additions & 9 deletions

File tree

packages/google-api-core/google/api_core/_observability.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,25 @@ def client_request_hook(span: Any, request: Any) -> None:
184184
def _grpc_client_response_hook(span: Any, response: Any) -> None:
185185
"""OpenTelemetry gRPC client response hook to record response status code.
186186
187-
Note: Upstream OpenTelemetry gRPC instrumentation only invokes this response_hook
188-
on successful RPC invocations. Failed RPCs raise an exception before this hook is reached.
189-
190187
Args:
191188
span: The OpenTelemetry span.
192189
response: The gRPC response object or details.
193190
"""
194-
if span is not None and hasattr(span, "set_attribute"):
195-
span.set_attribute("rpc.response.status_code", "OK")
191+
if not span.is_recording():
192+
return
193+
194+
# Verify the RPC succeeded before recording the OK response status.
195+
# Upstream async instrumentation invokes this hook on both successes
196+
# and failures, so check whether an error status was already recorded.
197+
status = getattr(span, "status", None)
198+
status_code = getattr(status, "status_code", None)
199+
if (
200+
getattr(status_code, "name", None) == "ERROR"
201+
or getattr(status_code, "value", None) == 2
202+
):
203+
return
204+
205+
span.set_attribute("rpc.response.status_code", "OK")
196206

197207

198208
def _get_tracer_provider(

packages/google-api-core/tests/unit/test_observability.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -531,11 +531,33 @@ def test_get_otel_async_interceptor_with_api_endpoint(monkeypatch):
531531
def test_grpc_client_response_hook_success():
532532
"""Proves that _grpc_client_response_hook sets rpc.response.status_code to 'OK' on success."""
533533
mock_span = mock.Mock()
534+
mock_span.is_recording.return_value = True
534535
_observability._grpc_client_response_hook(mock_span, mock.Mock())
535536
mock_span.set_attribute.assert_called_once_with("rpc.response.status_code", "OK")
536537

537538

538-
def test_grpc_client_response_hook_none_or_missing_set_attribute():
539-
"""Proves that _grpc_client_response_hook handles None or invalid span gracefully."""
540-
_observability._grpc_client_response_hook(None, mock.Mock())
541-
_observability._grpc_client_response_hook(object(), mock.Mock())
539+
def test_grpc_client_response_hook_not_recording():
540+
"""Proves that _grpc_client_response_hook skips non-recording spans."""
541+
mock_span = mock.Mock()
542+
mock_span.is_recording.return_value = False
543+
_observability._grpc_client_response_hook(mock_span, mock.Mock())
544+
mock_span.set_attribute.assert_not_called()
545+
546+
547+
def test_grpc_client_response_hook_error_status():
548+
"""Proves that _grpc_client_response_hook skips spans marked with ERROR status."""
549+
mock_span = mock.Mock()
550+
mock_span.is_recording.return_value = True
551+
mock_span.status.status_code.name = "ERROR"
552+
_observability._grpc_client_response_hook(mock_span, mock.Mock())
553+
mock_span.set_attribute.assert_not_called()
554+
555+
556+
def test_grpc_client_response_hook_error_status_value():
557+
"""Proves that _grpc_client_response_hook skips spans with StatusCode.ERROR value (2)."""
558+
mock_span = mock.Mock()
559+
mock_span.is_recording.return_value = True
560+
mock_span.status.status_code.name = "UNKNOWN"
561+
mock_span.status.status_code.value = 2
562+
_observability._grpc_client_response_hook(mock_span, mock.Mock())
563+
mock_span.set_attribute.assert_not_called()

0 commit comments

Comments
 (0)