Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 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
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,19 @@ else: # pragma: NO COVER

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.

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.

nit: can we change this to a TODO comment to remove the # type: ignore[misc] once libraries require google-api-core >= 2.42.0?

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

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.

It seems like this is only used in a test file. Can this live there, instead of in each generated package?


# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,19 @@ def record_error(self, exc: BaseException | None) -> None:

record_http_error = record_error

def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext:
# mypy: google-api-core < 2.42.0 lacks trace_http_request, pragma ignores the fallback function redefinition.
def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: # type: ignore[misc]
return _FallbackTraceContext()

# OpenTelemetry gRPC auto-instrumentation suppression was introduced in
# google-api-core 2.43.0+ to prevent redundant wire spans when OpenTelemetry
# instrumentation is active alongside Google Cloud SDK tracing.
# TODO(observability): Remove once setup.py.j2 enforces google-api-core >= 2.43.0.
HAS_AUTO_INSTRUMENTATION_SUPPRESSION = (
_observability is not None
and hasattr(_observability, "_AsyncSuppressingClientInterceptor")
)

# 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
128 changes: 127 additions & 1 deletion packages/gapic-generator/tests/system/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
from google.api_core._feature_gating_helpers import FeatureGatingError
from google.api_core.client_options import ClientOptions
from google.auth import credentials as ga_credentials
from google.showcase import EchoClient
from google.showcase import EchoAsyncClient, EchoClient
from google.showcase_v1beta1._compat import (
HAS_AUTO_INSTRUMENTATION_SUPPRESSION,
)

try:
from .conftest import construct_client
Expand Down Expand Up @@ -233,3 +236,126 @@ def test_env_var_opt_in(otel_echo_client):
assert len(spans) == 2
for span in spans:
assert span.name == "google.showcase.v1beta1.Echo/Echo"


@pytest.mark.skipif(
not HAS_AUTO_INSTRUMENTATION_SUPPRESSION,
reason="Installed google-api-core lacks auto-instrumentation suppression",
)
def test_auto_instrumentation_suppression_sync(span_exporter):
"""Verifies that upstream gRPC client auto-instrumentation spans are suppressed in sync calls.

When users enable global GrpcInstrumentorClient().instrument(), upstream injects a
generic interceptor into channel creation. This test verifies that our downstream
suppression interceptor prevents the duplicate, bare-bones upstream span from being
emitted while preserving the full Google Cloud SDK T4 span.
"""
grpc_instrumentation = pytest.importorskip("opentelemetry.instrumentation.grpc")

exporter, provider = span_exporter
instrumentor = grpc_instrumentation.GrpcInstrumentorClient()
instrumentor.instrument(tracer_provider=provider)
try:
options = ClientOptions(
api_endpoint="localhost:7469",
tracer_provider=provider,
)
with mock.patch.dict(
os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"}
):
channel = grpc.insecure_channel("localhost:7469")
transport = EchoClient.get_transport_class("grpc")(
channel=channel,
client_options=options,
credentials=ga_credentials.AnonymousCredentials(),
)
client = EchoClient(transport=transport, client_options=options)
response = client.echo(
showcase.EchoRequest(content="suppress sync duplicate")
)
assert response.content == "suppress sync duplicate"

spans = exporter.get_finished_spans()
assert len(spans) == 2

root_spans = [s for s in spans if s.parent is None]
child_spans = [s for s in spans if s.parent is not None]
assert len(root_spans) == 1
assert len(child_spans) == 1

root_span = root_spans[0]
child_span = child_spans[0]

assert root_span.name == "google.showcase.v1beta1.Echo/Echo"
assert child_span.name == "google.showcase.v1beta1.Echo/Echo"
assert child_span.parent.span_id == root_span.context.span_id

assert child_span.attributes.get("rpc.system.name") == "grpc"
assert child_span.attributes.get("server.address") == "localhost"
assert child_span.attributes.get("server.port") == 7469
assert child_span.attributes.get("url.domain") == "googleapis.com"
assert child_span.attributes.get("rpc.response.status_code") == "OK"

@chalmerlowe chalmerlowe Oct 9, 2026 •

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.

A good deal of the above is duplicative with test_auto_instrumentation_suppression_async()

It also happens to be duplicative with a number of the other existing tests.

I am putting together a PR with two standardized assertion functions to help reduce the duplication across the broader array of tests, but did not want to clutter up this PR.

finally:
instrumentor.uninstrument()


@pytest.mark.asyncio
@pytest.mark.skipif(
not HAS_AUTO_INSTRUMENTATION_SUPPRESSION,
reason="Installed google-api-core lacks auto-instrumentation suppression",
)
async def test_auto_instrumentation_suppression_async(span_exporter):
"""Verifies that upstream gRPC client auto-instrumentation spans are suppressed in async calls.

When users enable global GrpcAioInstrumentorClient().instrument(), upstream injects a
generic interceptor into aio channel creation. This test verifies that our downstream
suppression interceptor prevents the duplicate, bare-bones upstream span from being
emitted while preserving the full Google Cloud SDK T4 span.
"""
grpc_instrumentation = pytest.importorskip("opentelemetry.instrumentation.grpc")

exporter, provider = span_exporter
instrumentor = grpc_instrumentation.GrpcAioInstrumentorClient()
instrumentor.instrument(tracer_provider=provider)
try:
options = ClientOptions(
api_endpoint="localhost:7469",
tracer_provider=provider,
)
with mock.patch.dict(
os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"}
):
channel = grpc.aio.insecure_channel("localhost:7469")
transport = EchoAsyncClient.get_transport_class("grpc_asyncio")(
channel=channel,
client_options=options,
credentials=ga_credentials.AnonymousCredentials(),
)
client = EchoAsyncClient(transport=transport, client_options=options)
response = await client.echo(
showcase.EchoRequest(content="suppress async duplicate")
)
assert response.content == "suppress async duplicate"

spans = exporter.get_finished_spans()
assert len(spans) == 2

root_spans = [s for s in spans if s.parent is None]
child_spans = [s for s in spans if s.parent is not None]
assert len(root_spans) == 1
assert len(child_spans) == 1

root_span = root_spans[0]
child_span = child_spans[0]

assert root_span.name == "google.showcase.v1beta1.Echo/Echo"
assert child_span.name == "google.showcase.v1beta1.Echo/Echo"
assert child_span.parent.span_id == root_span.context.span_id

assert child_span.attributes.get("rpc.system.name") == "grpc"
assert child_span.attributes.get("server.address") == "localhost"
assert child_span.attributes.get("server.port") == 7469
assert child_span.attributes.get("url.domain") == "googleapis.com"
assert child_span.attributes.get("rpc.response.status_code") == "OK"
finally:
instrumentor.uninstrument()
Loading
Loading