Skip to content

feat(api_core): add sync and async gRPC transport attempt logging interceptors - #19010

Draft
chalmerlowe wants to merge 1 commit into
feat/otel-logging-m3-s2-logging-helpersfrom
feat/otel-logging-m3-s3-grpc-attempt-logging
Draft

chalmerlowe wants to merge 1 commit into
feat/otel-logging-m3-s2-logging-helpersfrom
feat/otel-logging-m3-s3-grpc-attempt-logging

Conversation

@chalmerlowe

Copy link
Copy Markdown
Contributor

Problem / Context

To capture transient wire failures and retries across synchronous and asynchronous gRPC transports without blocking in-flight grpc.Future or grpc.aio.UnaryUnaryCall objects, google.api_core._observability needs gRPC client interceptors that inspect completed RPC attempts and emit structured DEBUG logs correlated with the active Tier 4 OpenTelemetry span.

Bug: b/479827845

Solution

  • Add _GrpcAttemptLoggingInterceptor and _AsyncGrpcAttemptLoggingInterceptor in google/api_core/_observability.py to log failed unary-unary gRPC wire attempts at DEBUG level via log_transport_attempt.
  • Attach post-dispatch status and exception inspection via add_done_callback when supported on the returned gRPC response/call object so grpc.Future and grpc.aio.UnaryUnaryCall execution remains non-blocking.
  • Update get_otel_interceptor and get_otel_async_interceptor to sandwich the attempt logging interceptor between the outer OpenTelemetry tracing interceptor (so the Tier 4 span is active for trace correlation) and the inner _SuppressingClientInterceptor / _AsyncSuppressingClientInterceptor.
  • Add unit tests in tests/unit/test_observability.py covering sync/async response callbacks, direct exceptions, fail-open inspection errors, and interceptor ordering.

Verification

Relying on GitHub Actions CI for full matrix verification, alongside local unit test (unit-3.14) and lint verification.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces synchronous and asynchronous gRPC client interceptors (_GrpcAttemptLoggingInterceptor and _AsyncGrpcAttemptLoggingInterceptor) to log failed wire attempts at the DEBUG level, updating the OpenTelemetry interceptor retrieval functions to incorporate them. The review feedback recommends checking if the gRPC call or future has been cancelled before calling exception() or awaiting code() to avoid raising unhandled CancelledError or AioRpcError exceptions, ensuring robust fail-open behavior.

Comment on lines +915 to +916
exc_fn = getattr(resp, "exception", None)
if callable(exc_fn) and (resp_exc := exc_fn()) is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When implementing a completion callback for a future-like object, always check if the future was cancelled using cancelled() before calling exception() to avoid raising an unnecessary exception (such as CancelledError or FutureCancelledError).

                cancelled_fn = getattr(resp, "cancelled", None)
                if callable(cancelled_fn) and cancelled_fn():
                    return
                exc_fn = getattr(resp, "exception", None)
                if callable(exc_fn) and (resp_exc := exc_fn()) is not None:
References
  1. When implementing a completion callback for a concurrent.futures.Future, always check if the future was cancelled using future.cancelled() before calling future.exception() to avoid raising an unhandled CancelledError.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 42a6423 — broadened the fail-open catch in _inspect_sync_response to except (Exception, asyncio.CancelledError): and added a unit test covering cancelled Future inspection.


async def _inspect_async_call(target_call: Any) -> None:
try:
code_fn = getattr(target_call, "code", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For async gRPC calls, calling await code_fn() on a cancelled call can raise a CancelledError or AioRpcError. To prevent unnecessary exceptions and ensure a clean fail-open behavior, check if the call was cancelled using cancelled() before awaiting its code or details.

                cancelled_fn = getattr(target_call, "cancelled", None)
                if callable(cancelled_fn) and cancelled_fn():
                    return
                code_fn = getattr(target_call, "code", None)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 42a6423 — broadened the fail-open catch in _inspect_async_call to except (Exception, asyncio.CancelledError): and added a unit test covering cancelled async Call inspection.

@chalmerlowe
chalmerlowe force-pushed the feat/otel-logging-m3-s3-grpc-attempt-logging branch from 42a6423 to 0cbf1b7 Compare November 10, 2026 00:17
@chalmerlowe
chalmerlowe force-pushed the feat/otel-logging-m3-s2-logging-helpers branch from 866406e to 56c1e04 Compare November 10, 2026 00:17

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant