feat(observability): [WIP] implement universal 4-path OpenTelemetry tracing - #18433
chalmerlowe wants to merge 55 commits into
Conversation
… hook - Add rpc.system.name: 'grpc' - Extract server.address and server.port from client options endpoint - Extract gcp.grpc.resend_count from request resend count - Extract gcp.resource.destination.id from request name or parent - Add _client_response_hook for status code, error.type, and status.message - Plumb response_hook into get_otel_interceptor and get_otel_async_interceptor
- Test endpoint attribute parsing across host/port variations - Test destination id and resend count extraction - Test client request and response hooks covering all status and error cases - Test interceptor creation and custom endpoint attribute propagation - Achieve 100% statement and branch coverage on _observability.py
…and hooks - Rename _extract_t4_attributes to _extract_grpc_request_attributes - Rename _make_client_request_hook to _make_grpc_client_request_hook - Rename _client_request_hook to _grpc_client_request_hook - Rename _client_response_hook to _grpc_client_response_hook - Preserve generic _extract_endpoint_attributes for shared transport usage
…ntion - Rename test_extract_t4_attributes to test_extract_grpc_request_attributes - Rename test_client_request_hook to test_grpc_client_request_hook - Rename test_client_response_hook to test_grpc_client_response_hook - Update interceptor hook references to _grpc_client_* hooks
- Add url.domain extraction from universe_domain or default to googleapis.com - Add _extract_error_attributes helper to extract gcp.errors.domain and gcp.errors.metadata.<key> - Omit server.port when port matches scheme defaults (443 for https/grpc, 80 for http) - Remove redundant _grpc_client_response_hook and _STATUS_CODE_NAMES - Deduplicate name and parent resource lookup for gcp.resource.destination.id - Add comprehensive parametrized unit tests and update interceptor test suites
…tem attribute - Strip leading slash from gRPC attempt span names via span.update_name - Set rpc.method to the fully qualified method name per PRD specification - Retain rpc.system.name: 'grpc' and remove legacy rpc.system attribute to avoid duplication - Update unit tests to verify span name normalization and attribute deduplication
- Remove gcp.resource.destination.id extraction from _extract_grpc_request_attributes - Update unit tests to reflect attribute removal per July Strategy Update
…ments without grpc
…parsing, and attribute handling
…nv version in response hook
…and fix mypy comment
- Broaden transport check in client.py.j2 to allow gRPC transport subclasses. - Align version comments in client.py.j2 and grpc.py.j2 to 2.36.0+. - Synchronize all golden client and transport files with template updates. - Harden zero-overhead and custom tracer provider isolation assertions in test_tracing.py. - Add direct client initialization test to verify template injection end-to-end.
…port template - Place ClientInterceptor import under if TYPE_CHECKING: in grpc.py.j2 to eliminate runtime import overhead and avoid import failures on older google-api-core versions. - String-quote "ClientInterceptor" in the interceptors type annotation for GrpcTransport.__init__. - Regenerate and synchronize all golden gRPC transport files.
Align if TYPE_CHECKING: in golden gRPC transport files with # pragma: NO COVER to match grpc.py.j2 template output.
…t_options to wrapped methods
…or error.type in method tracing
…and client options
…ic-showcase # Conflicts: # packages/google-api-core/google/api_core/_observability.py # packages/google-api-core/tests/unit/test_observability.py
Pass canonical method_name to _wrap_method for mixin methods (Operations, IAM, Locations) so that client calls to mixin methods emit OpenTelemetry Tier 3 method spans.
…ementedError Update test comment in template and goldens to explain testing of NotImplementedError when accessing transport.kind.
…ons in system tracing tests Leverage otel_echo_client and span_exporter fixtures to eliminate boilerplate and unify span extraction patterns.
- Implement distributed OpenTelemetry tracing across all four GAPIC transports: sync gRPC, async gRPC, sync REST, and async REST. - Address Daniel Sanche review comments from PR #18342: * Remove dynamic inspect.signature introspection in favor of module-level constants. * Make BaseTransport.kind return empty string instead of raising NotImplementedError. * Remove unused inspect import in client.py.j2. * Uniformly pass client_options and kwargs across all REST and base transports. - Address PR #18367 async gRPC defects: * Respect grpc.aio channel immutability by passing interceptors during channel creation. * Fix caller-supplied compression parameter evaluation in method_async.py. * Capture asyncio.CancelledError on async callables for trace lifecycle safety. - Add HTTP wire tracing helpers (start_http_span, record_http_response, record_http_error) with W3C traceparent injection in google-api-core and Jinja macros. - Add comprehensive unit tests in google-api-core and gapic-generator.
There was a problem hiding this comment.
Code Review
This pull request implements OpenTelemetry tracing capabilities across gRPC and REST transports for generated clients. It introduces _observability modules, updates transport constructors to accept client_options, and wires tracing interceptors into the transport layers. The reviewer identified a critical issue in the _shared_macros.j2 template where start_http_span was called with incorrect arguments, which would cause a runtime TypeError. The suggested fix involves passing a properly constructed request object to the tracing utility.
| with _observability.start_http_span( | ||
| client_options, | ||
| method=method, | ||
| url=url, | ||
| url_template=uri, | ||
| headers=headers, | ||
| body=body, | ||
| ) as span: |
There was a problem hiding this comment.
The current call to _observability.start_http_span passes client_options as the first positional argument (which maps to request in the function signature) and passes method, url, headers, and body as keyword arguments. However, start_http_span does not accept these keyword arguments and expects a request object with those attributes. This will result in a TypeError at runtime when tracing is enabled.
To fix this, dynamically construct a lightweight request object using Python's built-in type() constructor and pass it as the request argument, while correctly passing url_template and client_options as keyword arguments.
with _observability.start_http_span(
type("Request", (), {"method": method, "url": url, "headers": headers, "body": body})(),
url_template=uri,
client_options=client_options,
) as span:
| "DeleteOperation", | ||
| request_type="operations_pb2.DeleteOperationRequest", | ||
| response_type="None", | ||
| rpc_name="google.longrunning.Operations/DeleteOperation", |
There was a problem hiding this comment.
Note
As context for the reviewer:
For native methods (like Echo or GetSecret), the generator reads the service's .proto file directly, so constructing the name in the template is straightforward:
method_name="{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}"However, mixins don't live in the service’s proto. Mixin methods (GetOperation, GetIamPolicy, ListLocations) are synthetic—they are injected by the generator from the static catalog in gapic/schema/mixins.py
Without a name attribute, any mixin call (like polling an operation or checking IAM permissions) would be unable to start an OpenTelemetry method span, or would emit an unknown/nameless span that failed our contract checks.
… flexible HTTP span invocations - Update start_http_span in google-api-core to accept both bundled request objects and unpacked keyword arguments (method, url, headers, body, client_options) to avoid dummy object overhead in GAPIC templates. - Properly unpack async gRPC interceptors into separate unary/stream lists in grpc_asyncio.py.j2. - Forward _client_options to RestStub and AsyncRestStub instances. - Supply method_name and is_streaming to async method wrappers in _shared_macros.j2 for Tier 3 method span creation. - Add download retries and offline caching for Showcase descriptors in noxfile.py. - Update system tracing tests and verify 100% pass rate against live Showcase across all 4 transports.
…bazel goldens - Accept BaseException in _observability.record_http_error for async cancellation safety - Dynamic attribute lookup for observability functions in Jinja templates to prevent mypy failures against older core - Unify HTTP dispatch pipeline in _shared_macros.j2 under span_context to ensure 100% statement and branch coverage across Python 3.10-3.14 - Add pragma NO COVER to version-dependent observability fallback branches - Regenerate Bazel integration test goldens for all 8 test suites
…ntegration baselines - Exclude packages/gapic-generator/tests/integration/goldens/ from pre-commit hooks to preserve byte-for-byte fidelity with Bazel outputs - Re-sync goldens cleanly via Bazel across all 8 integration suites
|
|
||
| import abc | ||
| import inspect | ||
| from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union |
There was a problem hiding this comment.
The focus for this file is to get fundamental/core elements into the Base class to:
- Help eliminate some checks that we were originally considering placing in the Transport classes, etc.
- Cut down on some of the boilerplate in the Transport classes
…async channel interceptors
…lize _observability compat
- Move _wrap_async_method into base.py.j2 alongside _wrap_method, delegating _wrap_method in grpc_asyncio and rest_asyncio to _wrap_async_method.
- Remove redundant wrap_async_method_macro from _shared_macros.j2.
- Add test_{service}_base_transport_wrap_async_method in test_%service.py.j2.
- Centralize _observability import in _compat.py.j2, replacing repetitive try/except blocks across client and all transport templates with clean _compat imports.
- Add test_observability_compat unit test in test_compat.py.j2.
- Regenerate and verify all 8 integration test goldens.
…est dispatch Introduce trace_http_request context manager in google.api_core._observability to manage HTTP span lifecycle and error recording automatically. Export trace_http_request and record_http_response from _compat with graceful no-op fallbacks for older versions of google-api-core. Refactor REST dispatch macro in _shared_macros.j2 to eliminate duck-typing and line-by-line coverage pragmas, and regenerate golden tests.
…pragmas, and streamline noxfile Consolidate _wrap_method and _wrap_async_method using a shared _wrap helper on base transport class in base.py.j2. Remove NO COVER pragmas from _compat.py.j2 observability block and add test_observability_compat_fallback in test_compat.py.j2 to test older environments. Streamline noxfile.py by reverting local caching logic to keep remote PR diff focused on OpenTelemetry dependencies. Regenerate all goldens.
Correctly route client interceptors to their corresponding channel interceptor lists (_unary_unary_interceptors, _unary_stream_interceptors, etc.) based on implemented methods. Update fallback helper in grpc_asyncio.py.j2 and regenerate integration goldens.
| } | ||
| {% endmacro %} | ||
|
|
||
| {# TODO: This helper logic to check whether `kind` needs to be configured in wrap_method |
There was a problem hiding this comment.
Comment for Reviewers:
This logic got moved to the base transport.
feat(observability): implement universal 4-path OpenTelemetry tracing
Problems Solved
Google Cloud Python client libraries support four communication paths: synchronous gRPC, asynchronous gRPC, synchronous REST (HTTP), and asynchronous REST (HTTP). Previously, distributed OpenTelemetry tracing was only wired for synchronous gRPC calls, leaving asynchronous and HTTP communications untraced. Additionally, earlier drafts of asynchronous tracing attempted to modify gRPC channels after creation, which violated the immutability rules of the underlying Python gRPC library, and did not consistently forward client options across all transport classes.
Solutions
This pull request provides a unified, cross-transport tracing implementation:
Universal 4-Transport Support:
GrpcTransport): Continues using OpenTelemetry gRPC channel interceptors.GrpcAsyncIOTransport): Supplies OpenTelemetry interceptors directly during channel creation, respecting the immutable design of asynchronous gRPC channels.RestTransport): Adds wire span tracking around HTTP requests with automatic W3C trace context header injection (traceparent).AsyncRestTransport): Integrates HTTP wire span tracking and async context lifecycle handling with W3C header propagation.Refined Transport Contracts & Cleanup:
BaseTransport.kindto safely return an empty string by default instead of raising an exception.asyncio.CancelledError) so spans are closed accurately when asynchronous tasks are cancelled.Notes for Reviewers
packages/gapic-generatorand core helper functions inpackages/google-api-core.