Repository navigation
feat(observability): suppress redundant gRPC auto-instrumentation spans #18592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
699f869
fbdbd73
1ffdedb
d0510c4
b27262d
0836e8b
8b3d292
488f21c
5115aea
a1d5dc1
4c6c9b4
4241832
989b77a
66b7b68
b538bea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
| 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") | ||
| ) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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" | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A good deal of the above is duplicative with 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() | ||
There was a problem hiding this comment.
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 requiregoogle-api-core >= 2.42.0?