Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
699f869
feat(observability): universal 4-path tracing in google-api-core and …
chalmerlowe Oct 6, 2026
fbdbd73
fix(observability): address review feedback on channel interceptors, …
chalmerlowe Oct 6, 2026
1ffdedb
test(gapic): update bazel integration goldens for resumable upload an…
chalmerlowe Oct 6, 2026
d0510c4
feat(observability): suppress redundant gRPC auto-instrumentation spans
chalmerlowe Oct 7, 2026
b27262d
Merge branch 'main' into feat/otel-tracing-suppress-auto-spans
chalmerlowe Oct 8, 2026
0836e8b
fix(observability): optimize hot path suppression and resolve syntax …
chalmerlowe Oct 8, 2026
8b3d292
fix(observability): resolve mypy invalid base class errors
chalmerlowe Oct 8, 2026
488f21c
style(observability): group dummy fallback class comments under singl…
chalmerlowe Oct 8, 2026
5115aea
test(core): cover otel fallback interceptor prepending in grpc_helper…
chalmerlowe Oct 8, 2026
a1d5dc1
refactor(observability): simplify interceptor attachment and consolid…
chalmerlowe Oct 8, 2026
4c6c9b4
fix(generator): resolve showcase mypy redefinition and gate suppressi…
chalmerlowe Oct 8, 2026
4241832
fix(observability): add coverage pragma and tests for intercept_chann…
chalmerlowe Oct 8, 2026
989b77a
chore(observability): polish comments and align core version references
chalmerlowe Oct 9, 2026
66b7b68
feat(observability): align suppression constants and add single inter…
chalmerlowe Oct 9, 2026
b538bea
test(generator): update integration goldens with suppression capabili…
chalmerlowe Oct 9, 2026
0238c81
fix(observability): adopt slots, explicit interceptor prepending, and…
chalmerlowe Oct 12, 2026
6283290
fix(gapic): guard prepend argument for older api-core and add mypy No…
chalmerlowe Oct 12, 2026
381b22a
fix(gapic): add coverage pragma and prepend test for async channel in…
chalmerlowe Oct 12, 2026
4cf2c59
refactor(observability): simplify channel interceptor registration an…
chalmerlowe Oct 13, 2026
880b102
chore: synchronize all integration goldens with simplified _compat fa…
chalmerlowe Oct 13, 2026
2fd729f
test(observability): use skipif decorator for missing grpc in fallbac…
chalmerlowe Oct 13, 2026
ac983f4
Apply suggestion from @chalmerlowe
chalmerlowe Oct 13, 2026
c879444
Apply suggestion from @chalmerlowe
chalmerlowe Oct 13, 2026
427dad1
Merge remote-tracking branch 'origin/main' into feat/otel-tracing-sup…
chalmerlowe Oct 13, 2026
5f8cb51
Merge remote-tracking branch 'origin/feat/otel-tracing-suppress-auto-…
chalmerlowe Oct 13, 2026
faa5bbc
chore: synchronize all integration goldens with template TODO updates
chalmerlowe Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@ else: # pragma: NO COVER

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# TODO (Issue #18657): (mypy) remove pragma once google-api-core >= 2.42.0
# ignores the fallback function redefinition of trace_http_request
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()


# The `kind` parameter in gapic_v1.method_async.wrap_method was introduced in
# google-api-core 2.29.0 (PR #688) alongside _DEFAULT_ASYNC_TRANSPORT_KIND to prevent
# async REST callables from being wrapped with gRPC error handlers.
Expand Down Expand Up @@ -123,7 +126,9 @@ def apply_channel_interceptors(


def _fallback_apply_interceptors(
channel: Any, interceptors: Optional[Sequence[Any]] = None
channel: Any,
interceptors: Optional[Sequence[Any]] = None,
prepend: bool = False,
) -> Any: # pragma: NO COVER
"""Fallback helper to attach interceptors to an existing async gRPC channel.

Expand All @@ -142,6 +147,8 @@ def _fallback_apply_interceptors(
or mock).
interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor
instances or callables to register on the channel.
prepend (bool): Whether to prepend interceptors to the beginning of the
channel's pipeline (e.g. for outer tracing spans) instead of appending.

Returns:
Any: The channel instance with interceptors registered.
Expand All @@ -153,41 +160,69 @@ def _fallback_apply_interceptors(
("intercept_stream_stream", "_stream_stream_interceptors"),
)
if interceptors:
for interceptor in interceptors:
matched = False

# Path 1: Interface-matched routing.
# Inspect whether the interceptor implements specific gRPC method hooks
# (unary-unary, unary-stream, stream-unary, stream-stream). If the channel
# maintains a corresponding internal list, append the interceptor (preventing duplicates).
# A single interceptor can handle multiple call patterns, so check all mappings.
for method_name, attr_name in mapping:
if hasattr(interceptor, method_name) and hasattr(channel, attr_name):
target_list = getattr(channel, attr_name)
if isinstance(target_list, list) and interceptor not in target_list:
target_list.append(interceptor)
matched = True

# Path 2: Duck-typed / callable fallback.
# If the interceptor did not implement any of the specific method signatures
# above (e.g., generic callables, untyped wrappers, or test doubles), fall back
# to registering it in the primary unary-unary interceptor list if present.
if not matched and hasattr(channel, "_unary_unary_interceptors"):
unary_interceptors = channel._unary_unary_interceptors
if isinstance(unary_interceptors, list) and interceptor not in unary_interceptors:
unary_interceptors.append(interceptor)
# Path 1: Typed interceptors matching specific gRPC RPC methods.
for method_name, attr_name in mapping:
target_list = getattr(channel, attr_name, None)
if not isinstance(target_list, list):
continue

matching = [
i
for i in interceptors
if hasattr(i, method_name) and i not in target_list
]
if matching:
if prepend:
target_list[:0] = matching
else:
target_list.extend(matching)

# Path 2: Fallback for generic/untyped interceptors.
# Interceptors that do not define any of the 4 standard gRPC method signatures
# (such as generic callables, custom wrappers, or test doubles) are routed to
# the primary unary-unary interceptor list.
unary_list = getattr(channel, "_unary_unary_interceptors", None)
if isinstance(unary_list, list):
standard_methods = (
"intercept_unary_unary",
"intercept_unary_stream",
"intercept_stream_unary",
"intercept_stream_stream",
)
fallback_items = [
i
for i in interceptors
if not any(hasattr(i, m) for m in standard_methods)
and i not in unary_list
]
if fallback_items:
if prepend:
unary_list[:0] = fallback_items
else:
unary_list.extend(fallback_items)
return channel


def apply_async_channel_interceptors(
channel: Any, interceptors: Optional[Sequence[Any]] = None
) -> Any:
channel: Any,
interceptors: Optional[Sequence[Any]] = None,
prepend: bool = False,
) -> Any: # pragma: NO COVER
"""Applies interceptors to an asynchronous gRPC channel with fallback for older google-api-core."""
if grpc_helpers_async is not None and hasattr(
grpc_helpers_async, "apply_channel_interceptors"
):
return grpc_helpers_async.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER
return _fallback_apply_interceptors(channel, interceptors) # pragma: NO COVER
if not prepend:
return grpc_helpers_async.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER
try:
return grpc_helpers_async.apply_channel_interceptors( # pragma: NO COVER
channel, interceptors, prepend=True # type: ignore[call-arg]
)
except TypeError: # pragma: NO COVER
# Fallback for environments running older google-api-core (< 2.43.0)
# where grpc_helpers_async.apply_channel_interceptors lacks `prepend`.
return _fallback_apply_interceptors(channel, interceptors, prepend=True)
return _fallback_apply_interceptors(channel, interceptors, prepend=prepend) # pragma: NO COVER


apply_channel_interceptors_async = apply_async_channel_interceptors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,21 +333,28 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
],
)

# 1. User & Logging Interceptors (appended to channel)
channel_interceptors = list(interceptors) if interceptors else []
self._interceptor = _LoggingClientAIOInterceptor()
channel_interceptors.append(self._interceptor)
self._grpc_channel = apply_async_channel_interceptors(
self._grpc_channel, channel_interceptors
)

# 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel)
# Must be placed at the front of the channel's pipeline so that our SDK spans
# wrap outside and downstream generic auto-instrumentation is suppressed.
if (
_observability is not None
and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None
):
# NOTE: Coverage tool ignores async interceptors in environments running
# legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable.
# Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0.
# TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0.
otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER
channel_interceptors.extend(otel_list) # pragma: NO COVER

self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors)
self._grpc_channel = apply_async_channel_interceptors(
self._grpc_channel, otel_list, prepend=True
) # pragma: NO COVER

self._logged_channel = self._grpc_channel
# Wrap messages. This must be done after self._logged_channel exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,5 +609,7 @@ def test_apply_async_channel_interceptors_compat():
dummy_channel = mock.Mock()
result = apply_async_channel_interceptors(dummy_channel, None)
assert result is dummy_channel
result_prepend = apply_async_channel_interceptors(dummy_channel, None, prepend=True)
assert result_prepend is dummy_channel

{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,12 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# TODO (Issue #18657): (mypy) remove pragma once google-api-core >= 2.42.0
# ignores the fallback function redefinition of trace_http_request
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()


# The `kind` parameter in gapic_v1.method_async.wrap_method was introduced in
# google-api-core 2.29.0 (PR #688) alongside _DEFAULT_ASYNC_TRANSPORT_KIND to prevent
# async REST callables from being wrapped with gRPC error handlers.
Expand Down Expand Up @@ -114,7 +117,9 @@ def apply_channel_interceptors(


def _fallback_apply_interceptors(
channel: Any, interceptors: Optional[Sequence[Any]] = None
channel: Any,
interceptors: Optional[Sequence[Any]] = None,
prepend: bool = False,
) -> Any: # pragma: NO COVER
"""Fallback helper to attach interceptors to an existing async gRPC channel.

Expand All @@ -133,6 +138,8 @@ def _fallback_apply_interceptors(
or mock).
interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor
instances or callables to register on the channel.
prepend (bool): Whether to prepend interceptors to the beginning of the
channel's pipeline (e.g. for outer tracing spans) instead of appending.

Returns:
Any: The channel instance with interceptors registered.
Expand All @@ -144,41 +151,69 @@ def _fallback_apply_interceptors(
("intercept_stream_stream", "_stream_stream_interceptors"),
)
if interceptors:
for interceptor in interceptors:
matched = False

# Path 1: Interface-matched routing.
# Inspect whether the interceptor implements specific gRPC method hooks
# (unary-unary, unary-stream, stream-unary, stream-stream). If the channel
# maintains a corresponding internal list, append the interceptor (preventing duplicates).
# A single interceptor can handle multiple call patterns, so check all mappings.
for method_name, attr_name in mapping:
if hasattr(interceptor, method_name) and hasattr(channel, attr_name):
target_list = getattr(channel, attr_name)
if isinstance(target_list, list) and interceptor not in target_list:
target_list.append(interceptor)
matched = True

# Path 2: Duck-typed / callable fallback.
# If the interceptor did not implement any of the specific method signatures
# above (e.g., generic callables, untyped wrappers, or test doubles), fall back
# to registering it in the primary unary-unary interceptor list if present.
if not matched and hasattr(channel, "_unary_unary_interceptors"):
unary_interceptors = channel._unary_unary_interceptors
if isinstance(unary_interceptors, list) and interceptor not in unary_interceptors:
unary_interceptors.append(interceptor)
# Path 1: Typed interceptors matching specific gRPC RPC methods.
for method_name, attr_name in mapping:
target_list = getattr(channel, attr_name, None)
if not isinstance(target_list, list):
continue

matching = [
i
for i in interceptors
if hasattr(i, method_name) and i not in target_list
]
if matching:
if prepend:
target_list[:0] = matching
else:
target_list.extend(matching)

# Path 2: Fallback for generic/untyped interceptors.
# Interceptors that do not define any of the 4 standard gRPC method signatures
# (such as generic callables, custom wrappers, or test doubles) are routed to
# the primary unary-unary interceptor list.
unary_list = getattr(channel, "_unary_unary_interceptors", None)
if isinstance(unary_list, list):
standard_methods = (
"intercept_unary_unary",
"intercept_unary_stream",
"intercept_stream_unary",
"intercept_stream_stream",
)
fallback_items = [
i
for i in interceptors
if not any(hasattr(i, m) for m in standard_methods)
and i not in unary_list
]
if fallback_items:
if prepend:
unary_list[:0] = fallback_items
else:
unary_list.extend(fallback_items)
return channel


def apply_async_channel_interceptors(
channel: Any, interceptors: Optional[Sequence[Any]] = None
) -> Any:
channel: Any,
interceptors: Optional[Sequence[Any]] = None,
prepend: bool = False,
) -> Any: # pragma: NO COVER
"""Applies interceptors to an asynchronous gRPC channel with fallback for older google-api-core."""
if grpc_helpers_async is not None and hasattr(
grpc_helpers_async, "apply_channel_interceptors"
):
return grpc_helpers_async.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER
return _fallback_apply_interceptors(channel, interceptors) # pragma: NO COVER
if not prepend:
return grpc_helpers_async.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER
try:
return grpc_helpers_async.apply_channel_interceptors( # pragma: NO COVER
channel, interceptors, prepend=True # type: ignore[call-arg]
)
except TypeError: # pragma: NO COVER
# Fallback for environments running older google-api-core (< 2.43.0)
# where grpc_helpers_async.apply_channel_interceptors lacks `prepend`.
return _fallback_apply_interceptors(channel, interceptors, prepend=True)
return _fallback_apply_interceptors(channel, interceptors, prepend=prepend) # pragma: NO COVER


apply_channel_interceptors_async = apply_async_channel_interceptors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,21 +319,28 @@ def __init__(self, *,
],
)

# 1. User & Logging Interceptors (appended to channel)
channel_interceptors = list(interceptors) if interceptors else []
self._interceptor = _LoggingClientAIOInterceptor()
channel_interceptors.append(self._interceptor)
self._grpc_channel = apply_async_channel_interceptors(
self._grpc_channel, channel_interceptors
)

# 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel)
# Must be placed at the front of the channel's pipeline so that our SDK spans
# wrap outside and downstream generic auto-instrumentation is suppressed.
if (
_observability is not None
and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None
):
# NOTE: Coverage tool ignores async interceptors in environments running
# legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable.
# Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0.
# TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0.
otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER
channel_interceptors.extend(otel_list) # pragma: NO COVER

self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors)
self._grpc_channel = apply_async_channel_interceptors(
self._grpc_channel, otel_list, prepend=True
) # pragma: NO COVER

self._logged_channel = self._grpc_channel
# Wrap messages. This must be done after self._logged_channel exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,3 +502,5 @@ def test_apply_async_channel_interceptors_compat():
dummy_channel = mock.Mock()
result = apply_async_channel_interceptors(dummy_channel, None)
assert result is dummy_channel
result_prepend = apply_async_channel_interceptors(dummy_channel, None, prepend=True)
assert result_prepend is dummy_channel
Loading
Loading