From 699f8691b5174e1d98e2e2296b5e77e313d72f3e Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 6 Oct 2026 10:40:28 -0400 Subject: [PATCH 1/3] feat(observability): universal 4-path tracing in google-api-core and gapic-generator --- .../%name_%version/%sub/_compat.py.j2 | 145 ++- .../%sub/services/%service/_shared_macros.j2 | 97 +- .../%sub/services/%service/client.py.j2 | 67 +- .../services/%service/transports/base.py.j2 | 78 +- .../services/%service/transports/grpc.py.j2 | 38 +- .../%service/transports/grpc_asyncio.py.j2 | 43 +- .../services/%service/transports/rest.py.j2 | 17 +- .../%service/transports/rest_asyncio.py.j2 | 22 +- .../%service/transports/rest_base.py.j2 | 16 +- .../%name_%version/%sub/test_%service.py.j2 | 187 ++++ .../%name_%version/%sub/test_compat.py.j2 | 77 +- packages/gapic-generator/noxfile.py | 19 + .../gapic-generator/tests/system/conftest.py | 112 ++- .../system/telemetry_requirements_matrix.csv | 23 + .../tests/system/test_span_compliance.py | 832 ++++++++++++++++++ .../tests/system/test_tracing.py | 237 +++++ .../google/api_core/_observability.py | 335 ++++++- .../google/api_core/gapic_v1/method.py | 30 +- .../google/api_core/gapic_v1/method_async.py | 192 +++- .../google/api_core/grpc_helpers_async.py | 55 +- .../tests/asyncio/gapic/test_method_async.py | 321 +++++++ .../tests/asyncio/test_grpc_helpers_async.py | 91 +- .../tests/unit/gapic/test_method.py | 83 +- .../tests/unit/test_observability.py | 771 ++++++++++++++++ 24 files changed, 3720 insertions(+), 168 deletions(-) create mode 100644 packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv create mode 100644 packages/gapic-generator/tests/system/test_span_compliance.py create mode 100644 packages/gapic-generator/tests/system/test_tracing.py diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index aa450ddd327f..59c040fe4eab 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -5,12 +5,12 @@ """A compatibility module for older versions of google-api-core.""" {% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %} {# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): -Clean up this file/functions when the minimum supported version of +Clean up this file/functions when the minimum supported version of google-api-core has the functions in `_compat.py.j2`. #} -{# TODO(https://github.com/googleapis/google-cloud-python/issues/17884): -Add conditional logic to check if static code exists in google-api-core and use it from there, +{# TODO(https://github.com/googleapis/google-cloud-python/issues/17884): +Add conditional logic to check if static code exists in google-api-core and use it from there, falling back to the local implementation if not present. #} -{# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): +{# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): Backfill compatibility functions being removed from the client layer. #} import os @@ -20,23 +20,150 @@ import uuid import google.protobuf.message {% endif %} -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple {% if has_auto_populated_fields %} from typing import TYPE_CHECKING, Union - -if TYPE_CHECKING: # pragma: NO COVER - import proto # type: ignore[import-untyped] {% endif %} +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +{% if has_auto_populated_fields %} +if TYPE_CHECKING: # pragma: NO COVER + import proto # type: ignore[import-untyped] +{% endif %} + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +if grpc_helpers_async is not None and hasattr( + grpc_helpers_async, "apply_channel_interceptors" +): + apply_channel_interceptors = grpc_helpers_async.apply_channel_interceptors # pragma: NO COVER +else: # pragma: NO COVER + apply_channel_interceptors = _fallback_apply_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` - # is different than that of the fallback implementation below. This will be removed once + # is different than that of the fallback implementation below. This will be removed once # we bump the minimum supported version of google-auth. from google.auth.transport.mtls import should_use_client_cert # type: ignore except ImportError: # pragma: NO COVER diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index e6ce18bfb94c..dca5ad63bf30 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -159,30 +159,44 @@ def _get_http_options(): session, timeout, transcoded_request, - body=None): - + body=None, + client_options=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" + uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = {{ await_prefix }}getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=uri, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - {% if body_spec %} - data=body, - {% endif %} - {% if not is_async and is_streaming_method %} - {# NOTE: The underlying `requests` library used for making a sync request - # requires us to set `stream=True` to avoid loading the entire response - # into memory at once. For an async request, given its nature where it - # reads data chunk by chunk, this is not required. - #} - stream=True, - {% endif %} - ) - return response + body=body, + ) as trace_ctx: + response = {{ await_prefix }}getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + {% if body_spec %} + data=body, + {% endif %} + {% if not is_async and is_streaming_method %} + {# NOTE: The underlying `requests` library used for making a sync request + # requires us to set `stream=True` to avoid loading the entire response + # into memory at once. For an async request, given its nature where it + # reads data chunk by chunk, this is not required. + #} + stream=True, + {% endif %} + ) + trace_ctx.record_response(response) + return response {% endmacro %} {# rest_call_method_common includes the common code for a rest __call__ method to be @@ -240,13 +254,24 @@ def _get_http_options(): ) # Send the request - response = {{ await_prefix }}{{ async_class_prefix }}{{ service_name }}RestTransport._{{method_name}}._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request{% if body_spec %}, body{% endif %}) + response = {{ await_prefix }}{{ async_class_prefix }}{{ service_name }}RestTransport._{{method_name}}._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + {% if body_spec %} + body, + {% endif %} + client_options=getattr(self, "_client_options", None), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - {# Note: format_http_response_error takes in more parameters than from_http_response and the - latter only supports a response of type requests.Response. + {# Note: format_http_response_error takes in more parameters than from_http_response and the + latter only supports a response of type requests.Response. TODO: Clean up the sync response error handling and use format_http_response_error. See issue: https://github.com/googleapis/gapic-generator-python/issues/2116. #} {% if is_async %} @@ -364,10 +389,13 @@ return {{ client_output_ident }}( {% macro prep_wrapped_messages_async_method(api, service, is_rest_asyncio=False) %} {% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { {% for method in service.methods.values() if not is_rest_asyncio or rest_async_io_enabled or method.is_resumable_upload %} - self.{{ method.transport_safe_name|snake_case }}: self._wrap_method( + self.{{ method.transport_safe_name|snake_case }}: self._wrap_async_method( self.{{ method.transport_safe_name|snake_case }}, {% if method.retry %} default_retry=retries.AsyncRetry( @@ -390,6 +418,10 @@ def _prep_wrapped_messages(self, client_info): {% endif %} default_timeout={{ method.timeout }}, client_info=client_info, + method_name="{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}", + {% if method.client_streaming or method.server_streaming %} + is_streaming=True, + {% endif %} ), {% endfor %}{# service.methods.values() #} {% if not is_rest_asyncio or rest_async_io_enabled %} @@ -397,10 +429,11 @@ def _prep_wrapped_messages(self, client_info): {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2197): Use `transport_safe_name` similar # to what we do for non-mixin methods above. #} - self.{{ method_name|snake_case }}: self._wrap_method( + self.{{ method_name|snake_case }}: self._wrap_async_method( self.{{ method_name|snake_case }}, default_timeout=None, client_info=client_info, + method_name="{{ api.mixin_api_signatures[method_name].rpc_name }}", ), {% endfor %}{# method_name in api.mixin_api_methods.keys() #} {% endif %} @@ -503,11 +536,11 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: Override in a subclass to read or manipulate the response or metadata after it is returned by the {{ service.name }} server but before it is returned to user code. - + We recommend only using this `post_{{ method.name|snake_case }}_with_metadata` interceptor in new development instead of the `post_{{ method.name|snake_case }}` interceptor. When both interceptors are used, this `post_{{ method.name|snake_case }}_with_metadata` interceptor runs after the - `post_{{ method.name|snake_case }}` interceptor. The (possibly modified) response returned by + `post_{{ method.name|snake_case }}` interceptor. The (possibly modified) response returned by `post_{{ method.name|snake_case }}` will be passed to `post_{{ method.name|snake_case }}_with_metadata`. """ @@ -555,7 +588,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: @property def {{ name|snake_case }}(self): - return self.{{ name|make_private }}(self._session, self._host, self._interceptor) # type: ignore + return self.{{ name|make_private }}(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}}, {{ async_method_name_prefix }}{{service.name}}RestStub): def __hash__(self): @@ -563,7 +596,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} {% set body_spec = api.mixin_http_options["{}".format(name)][0].body %} {{ response_method(body_spec, is_async=is_async, is_streaming_method=None) | indent(4) }} - + {{ async_prefix }}def __call__(self, request: {{ sig.request_type }}, *, retry: OptionalRetry=gapic_v1.method.DEFAULT, @@ -589,7 +622,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} {% endif %} """ {{ rest_call_method_common(body_spec, name, service, is_async)|indent(4) }} - + {% if sig.response_type == "None" %} return {{ await_prefix }}self._interceptor.post_{{ name|snake_case }}(None) {% else %} @@ -611,7 +644,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} "payload": response_payload, "headers": dict(response.headers), "status": response.status_code, - } + } _LOGGER.debug( "Received response for {{ service.meta.address.proto_package_versioned }}.{{ service.async_client_name }}.{{ name }}", extra = { @@ -633,7 +666,7 @@ class {{ name|make_private }}(_Base{{ service.name }}RestTransport._Base{{name}} {% macro client_method_metadata_default_value() %}(){% endmacro %} -{% macro client_method_metadata_argument_doc() %}metadata ({{ client_method_metadata_type() }}): Key/value pairs which should be +{% macro client_method_metadata_argument_doc() %}metadata ({{ client_method_metadata_type() }}): Key/value pairs which should be sent along with the request as metadata. Normally, each value must be of type `str`, but for metadata keys ending with the suffix `-bin`, the corresponding values must be of type `bytes`.{% endmacro %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 423c8d61a2fa..85c6855ebce1 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -33,6 +33,7 @@ from google.api_core import gapic_v1 {% if service.has_resumable_upload_methods %} from google.api_core.resumable_transfer import ResumableUploadConfig {% endif %} +from {{package_path}}._compat import _observability from {{package_path}}._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id @@ -317,17 +318,17 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): client_cert_source = mtls.default_client_cert_source() return client_cert_source - + def _validate_universe_domain(self): """Validates client's and credentials' universe domains are consistent. - + Returns: bool: True iff the configured universe domain is valid. Raises: ValueError: If the configured universe domain is not valid. """ - + # NOTE (b/349488459): universe validation is disabled until further notice. return True @@ -358,21 +359,21 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): @property def api_endpoint(self) -> str: """Return the API endpoint used by the client instance. - + Returns: str: The API endpoint used by the client instance. """ return self._api_endpoint - + @property def universe_domain(self) -> str: """Return the universe domain used by the client instance. - + Returns: str: The universe domain used by the client instance. """ return self._universe_domain - + def __init__(self, *, credentials: Optional[ga_credentials.Credentials] = None, transport: Optional[Union[str, {{ service.name }}Transport, Callable[..., {{ service.name }}Transport]]] = None, @@ -400,8 +401,8 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): {% endif %} client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): Custom options for the client. - - 1. The ``api_endpoint`` property can be used to override the + + 1. The ``api_endpoint`` property can be used to override the default endpoint provided by the client when ``transport`` is not explicitly provided. Only if this property is not set and ``transport`` was not explicitly provided, the endpoint is @@ -418,7 +419,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): not provided, the default SSL client certificate will be used if present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not set, no client certificate will be used. - + 3. The ``universe_domain`` property can be used to override the default "googleapis.com" universe. Note that the ``api_endpoint`` property still takes precedence; and ``universe_domain`` is @@ -476,7 +477,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): self._transport = cast({{ service.name }}Transport, transport) self._api_endpoint = self._transport.host - self._api_endpoint = (self._api_endpoint or + self._api_endpoint = (self._api_endpoint or get_api_endpoint( api_override=self._client_options.api_endpoint, universe_domain=self._universe_domain, @@ -513,10 +514,14 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): raise core_exceptions.AsyncRestUnsupportedParameterError( # type: ignore f"The following provided parameters are not supported for `transport=rest_asyncio`: {', '.join(provided_unsupported_params)}" ) + client_options = None + if _observability is not None and _observability.is_otel_capabilities_enabled(self._client_options): # pragma: NO COVER + client_options = self._client_options # pragma: NO COVER self._transport = transport_init( credentials=credentials, host=self._api_endpoint, client_info=client_info, + **({"client_options": client_options} if client_options is not None else {}), ) return @@ -534,19 +539,33 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): else cast(Callable[..., {{ service.name }}Transport], transport) ) {% endif %} + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) - + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) + if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER _LOGGER.debug( @@ -830,7 +849,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): gapic_v1.routing_header.to_grpc_metadata( (("resource", request_pb.resource),)), ) - + # Validate the universe domain. self._validate_universe_domain() diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index a19c8a242463..a8a38d09cce7 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,6 +3,7 @@ {% block content %} import abc +import inspect from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} @@ -10,6 +11,7 @@ from {{package_path}} import gapic_version as package_version import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -56,6 +58,16 @@ from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + ser DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(gapic_version=package_version.__version__) DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ +# Check once at module load time whether google-api-core's wrap_methods support +# OpenTelemetry tracing arguments (client_options, method_name, is_streaming, kind) +# to avoid recurring inspect.signature latency during client instantiation. +_WRAP_METHOD_SUPPORTS_TRACING = ( + "client_options" in inspect.signature(gapic_v1.method.wrap_method).parameters +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = ( + "client_options" in inspect.signature(gapic_v1.method_async.wrap_method).parameters +) + class {{ service.name }}Transport(abc.ABC): """Abstract transport class for {{ service.name }}.""" @@ -78,6 +90,7 @@ class {{ service.name }}Transport(abc.ABC): client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -108,6 +121,9 @@ class {{ service.name }}Transport(abc.ABC): to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ {% if service.any_extended_operations_methods %} self._extended_operations_services: Dict[str, Any] = {} @@ -151,17 +167,68 @@ class {{ service.name }}Transport(abc.ABC): host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { {% for method in service.methods.values() %} - self.{{ method.transport_safe_name|snake_case }}: gapic_v1.method.wrap_method( + self.{{ method.transport_safe_name|snake_case }}: self._wrap_method( self.{{ method.transport_safe_name|snake_case }}, {% if method.retry %} default_retry=retries.Retry( @@ -184,13 +251,18 @@ class {{ service.name }}Transport(abc.ABC): {% endif %} default_timeout={{ method.timeout }}, client_info=client_info, + method_name="{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}", + {% if method.client_streaming or method.server_streaming %} + is_streaming=True, + {% endif %} ), {% endfor %}{# method in service.methods.values() #} {% for method_name in api.mixin_api_methods.keys() %} - self.{{ method_name|snake_case }}: gapic_v1.method.wrap_method( + self.{{ method_name|snake_case }}: self._wrap_method( self.{{ method_name|snake_case }}, default_timeout=None, client_info=client_info, + method_name="{{ api.mixin_api_signatures[method_name].rpc_name }}", ), {% endfor %} {# method_name in api.mixin_api_methods.keys() #} } diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 7edaf533e71e..67137ffa5354 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -9,7 +9,12 @@ import logging as std_logging import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} +from {{package_path}}._compat import _observability + +from google.api_core import client_options as client_options_lib {% if service.has_resumable_upload_methods %} from google.api_core import exceptions as core_exceptions from google.api_core import resumable_transfer @@ -57,6 +62,10 @@ from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO from .rest import {{ service.name }}RestTransport {% endif %} +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -87,7 +96,7 @@ class _LoggingClientInterceptor(grpc.UnaryUnaryClientInterceptor): # pragma: NO grpc_response = { "payload": response_payload, "metadata": metadata, - "status": "OK", + "status": "OK", } _LOGGER.debug( f"Received response for {client_call_details.method}.", @@ -130,6 +139,8 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -150,7 +161,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): ignored if a ``channel`` instance is provided. channel (Optional[Union[grpc.Channel, Callable[..., grpc.Channel]]]): A ``Channel`` instance through which to make calls, or a Callable - that constructs and returns one. If set to None, ``self.create_channel`` + that constructs and returns one. If set to None, ``self.create_channel`` is used to create the channel. If a Callable is given, it will be called with the same arguments as used in ``self.create_channel``. api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. @@ -180,6 +191,12 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. Note: applying interceptors requires google-api-core >= 2.36.0. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -247,6 +264,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -268,6 +286,22 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + apply_interceptors = getattr( + grpc_helpers, + "apply_channel_interceptors", + lambda channel, interceptors: channel, + ) + self._grpc_channel = apply_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index e597d1b55de4..e28a1c529570 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -5,13 +5,17 @@ {% block content %} {% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %} -import inspect import json import pickle import logging as std_logging import warnings from typing import {% if service.has_resumable_upload_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} +from {{package_path}}._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -65,6 +69,7 @@ except ImportError: # pragma: NO COVER HAS_ASYNC_REST = False {% endif %} + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -95,7 +100,7 @@ class _LoggingClientAIOInterceptor(grpc.aio.UnaryUnaryClientInterceptor): # pra grpc_response = { "payload": response_payload, "metadata": metadata, - "status": "OK", + "status": "OK", } _LOGGER.debug( f"Received response to rpc {client_call_details.method}.", @@ -181,6 +186,8 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -202,7 +209,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): are passed to :func:`google.auth.default`. channel (Optional[Union[aio.Channel, Callable[..., aio.Channel]]]): A ``Channel`` instance through which to make calls, or a Callable - that constructs and returns one. If set to None, ``self.create_channel`` + that constructs and returns one. If set to None, ``self.create_channel`` is used to create the channel. If a Callable is given, it will be called with the same arguments as used in ``self.create_channel``. api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. @@ -232,6 +239,12 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -298,6 +311,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -319,10 +333,23 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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_channel_interceptors(self._grpc_channel, channel_interceptors) + self._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -514,8 +541,6 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): {{ shared_macros.prep_wrapped_messages_async_method(api, service)|indent(4) }} - {{ shared_macros.wrap_async_method_macro()|indent(4) }} - {% if service.has_resumable_upload_methods %} async def close(self): await self._logged_channel.close() @@ -525,7 +550,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): def close(self): return self._logged_channel.close() {% endif %} - + @property def kind(self) -> str: return "grpc_asyncio" @@ -536,4 +561,4 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): __all__ = ( '{{ service.name }}GrpcAsyncIOTransport', ) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 3f4f3f4797b3..4544b1ad7b1d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -8,6 +8,7 @@ import json # type: ignore from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers @@ -18,7 +19,7 @@ import urllib.parse from google.api_core import resumable_transfer {% endif %} {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} -from {{package_path}}._compat import transcode_request +from {{package_path}}._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -73,6 +74,7 @@ class {{service.name}}RestStub: _session: AuthorizedSession _host: str _interceptor: {{ service.name }}RestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): @@ -101,6 +103,8 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): url_scheme: str = 'https', interceptor: Optional[{{ service.name }}RestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -146,6 +150,9 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -157,7 +164,9 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -290,7 +299,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) {% endif %}{# method.lro #} - {#- TODO(https://github.com/googleapis/gapic-generator-python/issues/2274): Add debug log before intercepting a request #} + {#- TODO(https://github.com/googleapis/gapic-generator-python/issues/2274): Add debug log before intercepting a request #} resp = self._interceptor.post_{{ method.name|snake_case }}(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_{{ method.name|snake_case }}_with_metadata(resp, response_metadata) @@ -347,7 +356,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% endif %} # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore + return self._{{method.name}}(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore {% endfor %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 8ff5ece64292..abad21c3caa0 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -22,6 +22,7 @@ from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions {% if service.has_resumable_upload_methods %} import urllib.parse from google.api_core import resumable_transfer @@ -40,7 +41,7 @@ from google.api_core import retry_async as retries from google.api_core import rest_helpers from google.api_core import rest_streaming_async # type: ignore {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} -from {{package_path}}._compat import transcode_request +from {{package_path}}._compat import transcode_request, trace_http_request import google.protobuf @@ -97,6 +98,7 @@ class Async{{service.name}}RestStub: _session: AsyncAuthorizedSession _host: str _interceptor: Async{{service.name}}RestInterceptor + _client_options: Optional[ClientOptions] = None class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): """Asynchronous REST backend transport for {{ service.name }}. @@ -116,6 +118,8 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[Async{{ service.name }}RestInterceptor] = None, + client_options: Optional[ClientOptions] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -143,6 +147,9 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): "http" can be specified. interceptor (Optional[Async{{ service.name }}RestInterceptor]): Interceptor used to manipulate requests, request metadata, and responses. + client_options (Optional[google.api_core.client_options.ClientOptions]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor super().__init__( @@ -151,7 +158,9 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): client_info=client_info, always_use_jwt_access=False, url_scheme=url_scheme, - api_audience=None + api_audience=None, + client_options=client_options, + **kwargs, ) {# Note: Type for creds is ignored because of incorrect type hint for creds in the client layer. # TODO(https://github.com/googleapis/gapic-generator-python/issues/2177): Remove `# type: ignore` once @@ -159,7 +168,6 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): #} self._session = AsyncAuthorizedSession(self._credentials) # type: ignore self._interceptor = interceptor or Async{{ service.name }}RestInterceptor() - self._wrap_with_kind = True self._prep_wrapped_messages(client_info) {% if service.has_lro and rest_async_io_enabled %} self._operations_client: Optional[operations_v1.AsyncOperationsRestClient] = None @@ -168,8 +176,6 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {{ shared_macros.prep_wrapped_messages_async_method(api, service, is_rest_asyncio=True)|indent(4) }} - {{ shared_macros.wrap_async_method_macro()|indent(4) }} - {% for method in service.methods.values()|sort(attribute="name") if rest_async_io_enabled or method.is_resumable_upload %} class {{ method.name|make_private }}(_Base{{ service.name }}RestTransport._Base{{method.name}}, Async{{service.name}}RestStub): def __hash__(self): @@ -286,7 +292,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% endif %}{# method.void #} {% endif %}{# method.is_resumable_upload #} - + {% else %} raise NotImplementedError( "Method {{ method.name }} is not available over REST transport" @@ -352,11 +358,11 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% else %} {{method.output.ident}}]: {% endif %} - return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore + return self._{{method.name}}(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore {% endfor %} {% if rest_async_io_enabled %} - {% for name, sig in api.mixin_api_signatures.items() %} + {% for name, sig in api.mixin_api_signatures.items() %} {{ shared_macros.generate_mixin_call_method(service, api, name, sig, is_async=True) | indent(4) }} {% endfor %} {% endif %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 index 005bdceaeff1..00c66d75a509 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 @@ -21,6 +21,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format {% if opts.add_iam_methods or api.has_iam_mixin %} @@ -40,8 +41,8 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union class _Base{{ service.name }}RestTransport({{service.name}}Transport): """Base REST backend transport for {{ service.name }}. - - Note: This class is not meant to be used directly. Use its sync and + + Note: This class is not meant to be used directly. Use its sync and async sub-classes instead. This class defines the same methods as the primary client, so the @@ -60,13 +61,15 @@ class _Base{{ service.name }}RestTransport({{service.name}}Transport): always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: host ({% if service.host %}Optional[str]{% else %}str{% endif %}): {{ ' ' }}The hostname to connect to {% if service.host %}(default: '{{ service.host }}'){% endif %}. {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2173): Type hint for credentials is - # set to `Any` to support async and sync credential types in the parent rest transport classes. + # set to `Any` to support async and sync credential types in the parent rest transport classes. # However, we should have a stronger type here such as an abstract base credentials # class leveraged by sync and async credential classes. #} @@ -85,6 +88,9 @@ class _Base{{ service.name }}RestTransport({{service.name}}Transport): url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -100,7 +106,9 @@ class _Base{{ service.name }}RestTransport({{service.name}}Transport): credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) {% for method in service.methods.values()|sort(attribute="name") %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 8240485ce167..d1ff027ddf73 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -64,6 +64,7 @@ from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + ser from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }} import {{ service.async_client_name }} {% endif %} from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }} import transports +from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.client import _observability from google.api_core import client_options from google.api_core import exceptions as core_exceptions @@ -905,6 +906,192 @@ def test_{{ service.client_name|snake_case }}_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_{{ service.client_name|snake_case }}_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.{{ service.grpc_transport_name }}, "__init__", return_value=None + ) as patched_transport_init, + ): + client = {{ service.client_name }}(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_{{ service.name|snake_case }}_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.{{ service.grpc_transport_name }}, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.{{ service.grpc_transport_name }}( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_{{ service.name|snake_case }}_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.{{ service.grpc_transport_name }}, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.{{ service.grpc_transport_name }}( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_{{ service.name|snake_case }}_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.{{ service.grpc_transport_name }}( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.{{ service.grpc_asyncio_transport_name }}, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.{{ service.grpc_asyncio_transport_name }}( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.{{ service.grpc_asyncio_transport_name }}, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.{{ service.grpc_asyncio_transport_name }}( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_{{ service.name|snake_case }}_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.{{ service.grpc_asyncio_transport_name }}, + "create_channel", + ) as mock_create_channel: + transport = transports.{{ service.grpc_asyncio_transport_name }}( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ ({{ service.client_name }}, transports.{{ service.grpc_transport_name }}, "grpc", grpc_helpers), ({{ service.async_client_name }}, transports.{{ service.grpc_asyncio_transport_name }}, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 index 8d5744994305..07a154b8dcf0 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 @@ -5,9 +5,9 @@ {% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %} """Tests for the compatibility module for older versions of google-api-core.""" {# TODO(https://github.com/googleapis/google-cloud-python/issues/17813): -Clean up this file/tests when the minimum supported version of +Clean up this file/tests when the minimum supported version of google-api-core has the functions in `_compat.py.j2`. #} -{# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): +{# TODO(https://github.com/googleapis/google-cloud-python/issues/17883): Backfill compatibility functions tests being removed from the client layer. #} import json @@ -23,7 +23,16 @@ import google.auth.transport.mtls {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}}._compat import transcode_request -from {{package_path}}._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from {{package_path}}._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, +) {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id {% endif %} @@ -531,4 +540,66 @@ def test_read_environment_variables(): with pytest.raises(MutualTLSChannelError): read_environment_variables() + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from {{package_path}} import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from {{package_path}} import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + {% endblock %} diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index b9d1870b9e21..289be91feb81 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -278,6 +278,13 @@ def showcase_library( # Install grpcio-tools for protoc session.install("grpcio-tools") + # TODO(https://github.com/googleapis/gapic-generator-python/issues/2473): + # Warnings emitted from google-api-core starting in 2.28 + # appear to cause issues when running protoc. + # The specific failure is `Plugin output is unparseable` + if session.python == "3.10": + session.install("google-api-core<2.28") + # Install a client library for Showcase. with tempfile.TemporaryDirectory() as tmp_dir: # Download the Showcase descriptor. @@ -450,6 +457,11 @@ def showcase( # Use pytest-asyncio<1.0.0 while we investigate the recent failure described in # https://github.com/googleapis/gapic-generator-python/issues/2399 session.install("pytest", "pytest-asyncio<1.0.0") + session.install( + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation-grpc", + ) test_directory = Path("tests", "system") ignore_file = env.get("IGNORE_FILE") pytest_command = [ @@ -496,6 +508,11 @@ def showcase_w_rest_async( # Use pytest-asyncio<1.0.0 while we investigate the recent failure described in # https://github.com/googleapis/gapic-generator-python/issues/2399 session.install("pytest", "pytest-asyncio<1.0.0") + session.install( + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation-grpc", + ) test_directory = Path("tests", "system") ignore_file = env.get("IGNORE_FILE") pytest_command = [ @@ -590,6 +607,8 @@ def run_showcase_unit_tests( "pytest-cov", "pytest-xdist", "pytest-asyncio", + "opentelemetry-api", + "opentelemetry-sdk", ) # Freeze and print python environment package versions session.run("python", "-m", "pip", "freeze") diff --git a/packages/gapic-generator/tests/system/conftest.py b/packages/gapic-generator/tests/system/conftest.py index 73169dd8a79f..ac6de38eda0b 100644 --- a/packages/gapic-generator/tests/system/conftest.py +++ b/packages/gapic-generator/tests/system/conftest.py @@ -13,17 +13,21 @@ # limitations under the License. -import grpc -from unittest import mock import os -import pytest -import pytest_asyncio -from requests.adapters import HTTPAdapter - from typing import Sequence, Tuple +from unittest import mock +import grpc +import pytest +import pytest_asyncio from google.api_core.client_options import ClientOptions # type: ignore from google.showcase_v1beta1.services.echo.transports import EchoRestInterceptor +from requests.adapters import HTTPAdapter + +try: + from google.api_core import _observability +except ImportError: + _observability = None try: from google.auth.aio import credentials as ga_credentials_async @@ -34,20 +38,18 @@ HAS_GOOGLE_AUTH_AIO = False import google.auth from google.auth import credentials as ga_credentials -from google.showcase import EchoClient -from google.showcase import IdentityClient -from google.showcase import MessagingClient +from google.showcase import EchoClient, IdentityClient, MessagingClient if os.environ.get("GAPIC_PYTHON_ASYNC", "true") == "true": - from grpc.experimental import aio import asyncio - from google.showcase import EchoAsyncClient - from google.showcase import IdentityAsyncClient + + from google.showcase import EchoAsyncClient, IdentityAsyncClient + from grpc.experimental import aio try: from google.showcase_v1beta1.services.echo.transports import ( - AsyncEchoRestTransport, AsyncEchoRestInterceptor, + AsyncEchoRestTransport, ) HAS_ASYNC_REST_ECHO_TRANSPORT = True @@ -132,8 +134,8 @@ def callback(): return cert, key -client_options = ClientOptions() -client_options.client_cert_source = callback +default_mtls_client_options = ClientOptions() +default_mtls_client_options.client_cert_source = callback def pytest_addoption(parser): @@ -141,7 +143,9 @@ def pytest_addoption(parser): "--mtls", action="store_true", help="Run system test with mutual TLS channel" ) parser.addoption( - "--tls", action="store_true", help="Run system test with standard one-way TLS channel" + "--tls", + action="store_true", + help="Run system test with standard one-way TLS channel", ) @@ -153,6 +157,7 @@ def construct_client( channel_creator=grpc.insecure_channel, # for grpc,grpc_asyncio only credentials=ga_credentials.AnonymousCredentials(), transport_endpoint="localhost:7469", + client_options=None, ): if use_mtls: with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): @@ -162,7 +167,7 @@ def construct_client( mock_ssl_cred.return_value = ssl_credentials client = client_class( credentials=credentials, - client_options=client_options, + client_options=client_options or default_mtls_client_options, ) mock_ssl_cred.assert_called_once_with( certificate_chain=cert, private_key=key @@ -173,21 +178,24 @@ def construct_client( if transport_name in ["grpc", "grpc_asyncio"]: # TODO(gapic-generator-python/issues/1914): Need to test grpc transports without a channel_creator assert channel_creator - transport = transport_cls( - credentials=credentials, - channel=channel_creator(transport_endpoint), - ) + transport_kwargs = { + "credentials": credentials, + "channel": channel_creator(transport_endpoint), + "client_options": client_options, + } + transport = transport_cls(**transport_kwargs) elif transport_name in ["rest", "rest_asyncio"]: # The custom host explicitly bypasses https. transport = transport_cls( credentials=credentials, host=transport_endpoint, url_scheme="http", + client_options=client_options, ) else: raise RuntimeError(f"Unexpected transport type: {transport_name}") - client = client_class(transport=transport) + client = client_class(transport=transport, client_options=client_options) return client @@ -340,7 +348,9 @@ def _read_response_metadata_stream(self): def intercept_unary_unary(self, continuation, client_call_details, request): self._add_request_metadata(client_call_details) response = continuation(client_call_details, request) - metadata = [(k, str(v)) for k, v in response.initial_metadata()] + [(k, str(v)) for k, v in response.trailing_metadata()] + metadata = [(k, str(v)) for k, v in response.initial_metadata()] + [ + (k, str(v)) for k, v in response.trailing_metadata() + ] self.response_metadata = metadata return response @@ -399,7 +409,9 @@ async def _add_request_metadata(self, client_call_details): async def intercept_unary_unary(self, continuation, client_call_details, request): await self._add_request_metadata(client_call_details) response = await continuation(client_call_details, request) - metadata = [(k, str(v)) for k, v in await response.initial_metadata()] + [(k, str(v)) for k, v in await response.trailing_metadata()] + metadata = [(k, str(v)) for k, v in await response.initial_metadata()] + [ + (k, str(v)) for k, v in await response.trailing_metadata() + ] self.response_metadata = metadata return response @@ -458,9 +470,13 @@ async def intercepted_echo_grpc_async(use_mtls, use_tls): ) host = "localhost:7469" if use_mtls: - channel = grpc.aio.secure_channel(host, ssl_credentials, interceptors=[interceptor]) + channel = grpc.aio.secure_channel( + host, ssl_credentials, interceptors=[interceptor] + ) elif use_tls: - channel = grpc.aio.secure_channel(host, tls_credentials, interceptors=[interceptor]) + channel = grpc.aio.secure_channel( + host, tls_credentials, interceptors=[interceptor] + ) else: channel = grpc.aio.insecure_channel(host, interceptors=[interceptor]) transport = EchoAsyncClient.get_transport_class("grpc_asyncio")( @@ -472,6 +488,7 @@ async def intercepted_echo_grpc_async(use_mtls, use_tls): class HostNameIgnoringAdapter(HTTPAdapter): """Custom HTTPAdapter that disables hostname verification for local self-signed certs.""" + def cert_verify(self, conn, url, verify, cert): super().cert_verify(conn, url, verify, cert) conn.assert_hostname = False @@ -516,3 +533,46 @@ def intercepted_echo_rest_async(): ) return EchoAsyncClient(transport=transport), interceptor + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + """Prints a Telemetry Span Compliance summary to the console. + + Enables developers to view and copy all compliance test names and + pass/fail/error states directly from CI/console output without enabling + verbose output for unrelated tests. + """ + reports = ( + terminalreporter.getreports("passed") + + terminalreporter.getreports("failed") + + terminalreporter.getreports("skipped") + + terminalreporter.getreports("error") + ) + compliance_reports = [ + r + for r in reports + if "test_span_compliance.py" in r.nodeid + and (r.when == "call" or r.failed or r.skipped) + ] + if not compliance_reports: + return + + # Aggregate status per test nodeid so setup/call/teardown don't + # produce duplicate lines. Priority: ERROR > FAILED > SKIPPED > PASSED + status_by_test = {} + for rep in compliance_reports: + test_name = rep.nodeid.split("::")[-1] + if rep.when != "call" and rep.failed: + rep_status = "ERROR" + else: + rep_status = rep.outcome.upper() + + current = status_by_test.get(test_name) + if current is None or rep_status in ("ERROR", "FAILED"): + status_by_test[test_name] = rep_status + + terminalreporter.section( + "Telemetry Span Compliance Verification", sep="=", green=True + ) + for test_name, status in status_by_test.items(): + terminalreporter.write_line(f"[{status:6}] {test_name}") diff --git a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv new file mode 100644 index 000000000000..9e6841643553 --- /dev/null +++ b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv @@ -0,0 +1,23 @@ +Feature ID,Feature Name,Tier,Transport,Scenario,Span Count,Span Kind,Span Name,Span Status,rpc.system.name,rpc.method,rpc.response.status_code,http.request.method,http.response.status_code,rpc.grpc.status_code,url.domain,url.template,server.address,server.port,error.type,status.message,resend_count,parent_span_id,Extra / Permitted Attributes +F1.1,Tracing Off (HTTP),T4,HTTP/REST,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted +F1.2,Unary Success (HTTP),T4,HTTP/REST,Happy Path,1,CLIENT,POST,OK,NOT SET,NOT SET,NOT SET,POST,200,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"url.full, http.request.body.size" +F1.3,Server Error (HTTP),T4,HTTP/REST,Server Failure,1,CLIENT,POST,ERROR,NOT SET,NOT SET,NOT SET,POST,400,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"url.full, http.request.body.size" +F1.4,Client Timeout (HTTP),T4,HTTP/REST,Client Timeout,1,CLIENT,POST,ERROR,NOT SET,NOT SET,NOT SET,POST,504,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full +F1.5,Retry Recovery (HTTP),T4,HTTP/REST,Retry Recovery,2,CLIENT,POST,ERROR | OK,NOT SET,NOT SET,NOT SET,POST,503 | 200,NOT SET,googleapis.com,/v1beta1/sequences/*,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full +F1.6,Tracing Off (gRPC),T4,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted +F1.7,Unary Success (gRPC),T4,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.8,Server Error (gRPC),T4,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,3,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.9,Client Timeout (gRPC),T4,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,4,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.10,Retry Recovery (gRPC),T4,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR | UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET | OK,NOT SET,NOT SET,14 | 0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F2.1,Tracing Off (HTTP),T3,HTTP/REST,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted +F2.2,Unary Success (HTTP),T3,HTTP/REST,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,http,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) +F2.3,Server Error (HTTP),T3,HTTP/REST,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,http,google.showcase.v1beta1.Echo/Echo,BAD_REQUEST,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,BAD_REQUEST,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" +F2.4,Client Timeout (HTTP),T3,HTTP/REST,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,http,google.showcase.v1beta1.Echo/Echo,GATEWAY_TIMEOUT,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,GATEWAY_TIMEOUT,Client deadline exceeded,NOT SET,None (Root),gcp.errors.domain +F2.5,Tracing Off (gRPC),T3,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted +F2.6,Unary Success (gRPC),T3,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) +F2.7,Server Error (gRPC),T3,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,INVALID_ARGUMENT,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,INVALID_ARGUMENT,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" +F2.8,Client Timeout (gRPC),T3,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,DEADLINE_EXCEEDED,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,DEADLINE_EXCEEDED,Client deadline exceeded,NOT SET,None (Root),gcp.errors.domain +F3.1,Retry Succeeds (HTTP),T3 + T4,HTTP/REST,Retry Recovery,3,CLIENT,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: POST,T3: UNSET | T4: ERROR | OK,T3: http | T4: NOT SET,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: NOT SET,T3: OK | T4: NOT SET,T3: NOT SET | T4: POST,T3: NOT SET | T4: 503 | 200,NOT SET,T3: NOT SET | T4: googleapis.com,T3: NOT SET | T4: /v1beta1/sequences/*,T3: NOT SET | T4: localhost,T3: NOT SET | T4: 7469,NOT SET,NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.2,Retries Exhausted (HTTP),T3 + T4,HTTP/REST,Retries Exhausted,N + 1,CLIENT,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: POST,T3: ERROR | T4: ERROR,T3: http | T4: NOT SET,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: NOT SET,T3: UNAVAILABLE | T4: NOT SET,T3: NOT SET | T4: POST,T3: NOT SET | T4: 503,NOT SET,T3: NOT SET | T4: googleapis.com,T3: NOT SET | T4: /v1beta1/sequences/*,T3: NOT SET | T4: localhost,T3: NOT SET | T4: 7469,T3: UNAVAILABLE | T4: NOT SET,T3: Persistent outage | T4: NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.3,Retry Succeeds (gRPC),T3 + T4,gRPC,Retry Recovery,3,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: UNSET | T4: ERROR | UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: OK | T4: NOT SET | OK,NOT SET,NOT SET,T3: NOT SET | T4: 14 | 0,T3: NOT SET | T4: googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.4,Retries Exhausted (gRPC),T3 + T4,gRPC,Retries Exhausted,N + 1,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: ERROR | T4: ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: UNAVAILABLE | T4: NOT SET,NOT SET,NOT SET,T3: NOT SET | T4: 14,T3: NOT SET | T4: googleapis.com,NOT SET,NOT SET,NOT SET,T3: UNAVAILABLE | T4: NOT SET,T3: Persistent outage | T4: NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 diff --git a/packages/gapic-generator/tests/system/test_span_compliance.py b/packages/gapic-generator/tests/system/test_span_compliance.py new file mode 100644 index 000000000000..3949f4e29fea --- /dev/null +++ b/packages/gapic-generator/tests/system/test_span_compliance.py @@ -0,0 +1,832 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Telemetry Semantic Compliance Verification Suite. + +This module implements a spec-driven, matrix-governed verification engine for OpenTelemetry +tracing across Google Cloud Client Libraries (GAPIC), evaluated live against the GAPIC Showcase daemon. + +Architectural Overview: +----------------------- +Rather than hardcoding dozens of repetitive test functions with fragile assertions, this suite +operates as a data-driven test engine governed by a Single Source of Truth (SSOT): +`telemetry_requirements_matrix.csv`. + +The engine operates across five distinct phases: + +1. Specification Matrix (`telemetry_requirements_matrix.csv`): + Defines the contract for all 22 required observability features across transports (gRPC, REST), + span tiers (T3 Logical Client vs. T4 Wire Attempt), error scenarios, and retry sequences. + +2. Scenario Execution & Span Capture (`execute_scenario`): + Instantiates isolated Showcase clients configured with in-memory OpenTelemetry tracer + providers and dispatches live requests (unary echo calls, stateful multi-step retry sequences) + against the Showcase test daemon. + +3. Hierarchy Classification: + Partitions captured finished spans by parentage: + - Root Spans (parent is None): Represents the outer logical RPC call (T3 Tier). + - Child Spans (parent is not None): Represents physical wire attempts (T4 Tier). + Enforces the hierarchical invariant that all T4 spans must link directly to the T3 span ID. + +4. Cell Grammar Parser & Sub-Validators (`resolve_expected_value`, `assert_span_matches_row`): + Parses matrix cell expressions (supporting static values, positional sequences across attempts + such as '503 | 200', and tier-partitioned expressions such as 'T3: OK | T4: NOT SET'). + Validates span names, kinds, status codes, string attributes (with wildcard support), + integer attributes, substring status messages, and protocol-specific resend counts. + +5. Diagnostic Archival (`RAW_SPANS_CATALOG` & `dump_raw_spans` fixture): + Serializes every captured span in full JSON format to `raw_spans_output.json`, enabling + offline auditability and regression post-mortems without re-running the live server. +""" + +from __future__ import annotations + +import contextlib +import csv +import json +from pathlib import Path +from typing import Any + +import pytest + +try: + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + + HAS_OPENTELEMETRY = True +except ImportError: + HAS_OPENTELEMETRY = False + +if not HAS_OPENTELEMETRY: + pytest.skip("OpenTelemetry is not installed", allow_module_level=True) + +try: + from google.api_core import _observability + + HAS_TIER3_TRACING = hasattr(_observability, "_TraceContext") +except ImportError: + HAS_TIER3_TRACING = False + +if not HAS_TIER3_TRACING: + pytest.skip( + "Installed google-api-core lacks Tier 3 OpenTelemetry tracing", + allow_module_level=True, + ) + +from google import showcase +from google.api_core import exceptions +from google.api_core import retry as retries +from google.api_core.client_options import ClientOptions +from google.rpc import code_pb2, status_pb2 +from google.showcase import ( + AttemptSequenceRequest, + CreateSequenceRequest, + EchoClient, + Sequence, + SequenceServiceClient, +) + +try: + from . import conftest +except (ImportError, ValueError): + import conftest + + +# --------------------------------------------------------------------------- +# Test Harness Performance & Tuning Knobs +# --------------------------------------------------------------------------- +# These constants govern execution timing and timeout budgets during test runs. +# They are intentionally separated from the telemetry specification itself: +# while production defaults use exponential backoffs and multi-second retry windows, +# test scenarios require ultra-fast, deterministic execution to keep the CI suite snappy +# without flaking under high concurrency. +SHORT_CLIENT_TIMEOUT_SECONDS = 0.2 +FAST_RETRY_BACKOFF_SECONDS = 0.01 +# Setting multiplier to 1.0 enforces constant/linear polling intervals, bypassing +# exponential backoff delay calculation during retry loops: +CONSTANT_BACKOFF_MULTIPLIER = 1.0 +FAST_EXHAUSTION_DEADLINE_SECONDS = 0.05 +GENEROUS_RECOVERY_DEADLINE_SECONDS = 5.0 +# Queue depth buffer configured on Showcase server for retry exhaustion tests: +SHOWCASE_EXHAUSTION_QUEUE_BUFFER = 20 + + +# --------------------------------------------------------------------------- +# CSV Matrix Loader & Diagnostic Archival +# --------------------------------------------------------------------------- + +CSV_PATH = Path(__file__).parent / "telemetry_requirements_matrix.csv" + + +def load_feature_matrix() -> dict[str, dict[str, str]]: + """Loads and indexes the Single Source of Truth CSV requirements matrix. + + Returns: + A dictionary mapping Feature ID (e.g. 'grpc_happy_path') to its dictionary + of column names and raw string specifications. + """ + with open(CSV_PATH, mode="r", encoding="utf-8") as f: + return {r["Feature ID"]: r for r in csv.DictReader(f)} + + +FEATURE_MATRIX = load_feature_matrix() +RAW_SPANS_CATALOG: dict[str, Any] = {} + + +# --------------------------------------------------------------------------- +# Test Fixture +# --------------------------------------------------------------------------- + + +@pytest.fixture +def span_exporter(monkeypatch): + """Provides an isolated OpenTelemetry in-memory span exporter and provider. + + Lifecycle: + 1. Instantiates an InMemorySpanExporter coupled to a SimpleSpanProcessor. + 2. Sets the SDK tracing feature flag ('GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED=true') + via pytest's monkeypatch fixture to guarantee process-level isolation without + polluting global environment state. + 3. Yields the (exporter, provider) pair to the test body. + 4. Clears all buffered spans on teardown to prevent state leakage between tests. + + Args: + monkeypatch: Pytest fixture for thread-safe environment variable management. + + Yields: + tuple[InMemorySpanExporter, TracerProvider]: The span collector and provider. + """ + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + yield exporter, provider + exporter.clear() + + +# --------------------------------------------------------------------------- +# RPC Invocation Runners & Client Construction +# --------------------------------------------------------------------------- + + +def construct_observability_client( + client_class, + transport: str, + client_options: ClientOptions | None = None, + use_mtls: bool = False, +): + """Factory helper to instantiate GAPIC Showcase clients with test harness defaults. + + This function encapsulates client instantiation by supplying `use_mtls=False` + by default (the standard for local Showcase daemon testing) while forwarding + transport configuration and OpenTelemetry client_options. + + Design Note: + Rather than modifying the shared system test fixture helper (`conftest.construct_client`), + wrapping it locally insulates this compliance suite with zero blast radius on existing + Showcase tests. + + Args: + client_class: The GAPIC client class to instantiate (e.g. EchoClient). + transport: Target transport name ('grpc' or 'rest'). + client_options: Optional ClientOptions containing the TracerProvider. + use_mtls: Whether to enable mTLS (defaults to False). + + Returns: + An instantiated, ready-to-call Showcase client instance. + """ + return conftest.construct_client( + client_class, + use_mtls=use_mtls, + transport_name=transport, + client_options=client_options, + ) + + +def run_echo_call(client: EchoClient, scenario: str): + """Executes unary RPC requests against an injected EchoClient. + + This runner exercises the three standard unary RPC lifecycle scenarios: + - 'Happy Path' / 'Tracing Off': Dispatches a successful unary EchoRequest. + - 'Server Failure': Injects a non-retryable INVALID_ARGUMENT gRPC status on the + Showcase server, expecting an immediate InvalidArgument or BadRequest exception. + - 'Client Timeout': Enforces a tight client-side timeout (`SHORT_CLIENT_TIMEOUT_SECONDS`) + with retries disabled, expecting a DeadlineExceeded or GatewayTimeout exception. + + Args: + client: The instantiated EchoClient (injected by `execute_scenario`). + scenario: The scenario name string from the CSV matrix row. + """ + if scenario in ("Happy Path", "Tracing Off"): + client.echo(showcase.EchoRequest(content="hello")) + elif scenario == "Server Failure": + with pytest.raises((exceptions.InvalidArgument, exceptions.BadRequest)): + client.echo( + showcase.EchoRequest( + error=status_pb2.Status( + code=code_pb2.INVALID_ARGUMENT, + message="Simulated unretryable invalid argument error.", + ) + ) + ) + elif scenario == "Client Timeout": + with pytest.raises((exceptions.DeadlineExceeded, exceptions.GatewayTimeout)): + client.echo( + showcase.EchoRequest( + error=status_pb2.Status( + code=code_pb2.DEADLINE_EXCEEDED, + message="Client deadline exceeded", + ) + ), + timeout=SHORT_CLIENT_TIMEOUT_SECONDS, + retry=None, + ) + + +def run_sequence_retry_call( + client: SequenceServiceClient, + scenario: str, + exporter: InMemorySpanExporter, +): + """Executes stateful retry sequence requests against an injected SequenceServiceClient. + + The Showcase SequenceService allows configuring a server-side queue of pre-programmed + responses that return sequentially across consecutive attempt RPCs. + + Execution Flow: + 1. Pre-programs the server-side sequence queue via `create_sequence`: + - 'Retries Exhausted': Configures an array of UNAVAILABLE ('Persistent outage') responses + and a tight deadline (`FAST_EXHAUSTION_DEADLINE_SECONDS`). + - 'Retry with Recovery': Configures one UNAVAILABLE ('Temporary glitch') response + followed by an OK response, and a generous deadline (`GENEROUS_RECOVERY_DEADLINE_SECONDS`). + 2. Flushes the in-memory span exporter (`exporter.clear()`). + CRITICAL: The `create_sequence` setup call emits its own OpenTelemetry spans! + Purging the exporter ensures that downstream assertions strictly evaluate spans emitted + by the target `attempt_sequence` call. + 3. Invokes `attempt_sequence` with a custom fast Retry policy. + Uses `contextlib.nullcontext()` as a "do-nothing" placeholder so we can run the test + call using a single `with expectation:` block. If we expect the call to fail, `expectation` + is `pytest.raises(...)` to catch the error. If we expect it to succeed, `expectation` is + `nullcontext()`, which just lets the code run normally. This saves us from having to + write out the client call twice! + + Args: + client: The instantiated SequenceServiceClient (injected by `execute_scenario`). + scenario: The scenario name string from the CSV matrix row. + exporter: The active span exporter used to purge pre-flight setup spans. + """ + is_exhaust = scenario == "Retries Exhausted" + + if is_exhaust: + responses = [ + Sequence.Response( + status=status_pb2.Status( + code=code_pb2.UNAVAILABLE, + message="Persistent outage", + ) + ) + ] * SHOWCASE_EXHAUSTION_QUEUE_BUFFER + deadline = FAST_EXHAUSTION_DEADLINE_SECONDS + expectation = pytest.raises( + (exceptions.RetryError, exceptions.ServiceUnavailable) + ) + else: + responses = [ + Sequence.Response( + status=status_pb2.Status( + code=code_pb2.UNAVAILABLE, + message="Temporary glitch", + ) + ), + Sequence.Response(status=status_pb2.Status(code=code_pb2.OK)), + ] + deadline = GENEROUS_RECOVERY_DEADLINE_SECONDS + expectation = contextlib.nullcontext() + + # Step 1: Pre-program response sequence on the Showcase daemon + seq = client.create_sequence( + CreateSequenceRequest(sequence=Sequence(responses=responses)) + ) + + # Step 2: Flush setup RPC spans so assertions evaluate only attempt_sequence + exporter.clear() + + # Step 3: Configure fast retry policy and execute sequence attempts + retry_policy = retries.Retry( + predicate=retries.if_exception_type(exceptions.ServiceUnavailable), + initial=FAST_RETRY_BACKOFF_SECONDS, + maximum=FAST_RETRY_BACKOFF_SECONDS, + multiplier=CONSTANT_BACKOFF_MULTIPLIER, + deadline=deadline, + ) + + with expectation: + client.attempt_sequence( + AttemptSequenceRequest(name=seq.name), + retry=retry_policy, + ) + + +def execute_scenario( + scenario: str, + transport_str: str, + provider: TracerProvider, + exporter: InMemorySpanExporter, + monkeypatch: pytest.MonkeyPatch, +): + """High-level scenario dispatcher and Dependency Injection (DI) coordinator. + + Responsibilities: + 1. Normalizes transport strings ('grpc' vs 'rest'). + 2. Injects the TracerProvider into `ClientOptions` (or unsets it for 'Tracing Off'). + 3. Handles scenario-specific environment configuration via `monkeypatch`. + 4. Instantiates the appropriate client class (`EchoClient` vs `SequenceServiceClient`). + 5. Dispatches execution to the corresponding runner (`run_echo_call` or `run_sequence_retry_call`). + + Args: + scenario: The scenario column value from the CSV matrix row. + transport_str: The transport column value ('gRPC' or 'REST'). + provider: The active OpenTelemetry TracerProvider. + exporter: The active InMemorySpanExporter. + monkeypatch: Pytest monkeypatch fixture for environment variable scoping. + """ + transport = "grpc" if "grpc" in transport_str.lower() else "rest" + client_options = ClientOptions(tracer_provider=provider) + + if scenario == "Tracing Off": + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + client_options = ClientOptions() + + if scenario in ("Happy Path", "Server Failure", "Client Timeout", "Tracing Off"): + client = construct_observability_client(EchoClient, transport, client_options) + run_echo_call(client, scenario) + else: + client = construct_observability_client( + SequenceServiceClient, transport, client_options + ) + run_sequence_retry_call(client, scenario, exporter) + + +# --------------------------------------------------------------------------- +# Universal Matrix-Driven Cell Parser & Assertion Engine +# --------------------------------------------------------------------------- + + +def resolve_expected_value( + raw_val: str | None, target_tier: str, attempt_idx: int = 0 +) -> str: + """Resolves the expected value from a CSV cell using the matrix mini-grammar. + + The CSV requirements matrix uses a compact domain-specific language (DSL) to encode + expectations across different span tiers (T3 vs T4) and retry attempt sequences + without exploding the number of columns. + + Supported Syntax Patterns: + -------------------------- + 1. Static Values: + - '200', 'POST', 'INTERNAL', 'NOT SET', 'N/A' + - Directly returns the string literal when the expected value is identical everywhere. + + 2. Positional Sequences (Across Retry Attempts): + - '503 | 200' + - 'ERROR | OK' + - When a call retries, each try (attempt 0, attempt 1, etc.) can produce a different result. + For example, the first try might fail with 503, but the second try succeeds with 200. + - The pipe ('|') separates what we expect on each consecutive try: + * Attempt 0 checks the 1st item ('503'). + * Attempt 1 checks the 2nd item ('200'). + - If there are more attempts than values listed (like a loop that keeps failing 20 times), + it holds onto the last item in the list. + + 3. Tier-Partitioned Expressions (Per-Layer Rules): + - 'T3: OK | T4: NOT SET' + - 'T3: UNSET | T4: ERROR | OK' + - A single row in our matrix often checks an attribute that behaves differently depending + on which layer of the software we look at: + * Tier 3 (T3): The outer, overall operation span. + * Tier 4 (T4): The inner, individual network attempt spans. + - We label each tier with 'T3:' or 'T4:', followed by the value (or positional sequence) + for that tier: + * 'T3: OK | T4: NOT SET' means: "The outer T3 span should be OK, but every inner T4 + attempt span should NOT have this attribute set." + * 'T3: UNSET | T4: ERROR | OK' means: "The outer T3 span should be UNSET, while the T4 + spans will see an ERROR on the 1st try and OK on the 2nd try." + - If a tier is not mentioned in the cell, it returns 'N/A' (meaning that tier doesn't care + about this attribute in this test). + + Args: + raw_val: Raw string content from the CSV cell (or None if empty). + target_tier: The span tier currently being evaluated ('T3' or 'T4'). + attempt_idx: Zero-based attempt index for child T4 spans (defaults to 0 for T3). + + Returns: + The resolved expected string value (e.g. 'OK', 'NOT SET', 'N/A'). + """ + if raw_val is None: + return "N/A" + raw = raw_val.strip() + if not raw or raw == "N/A": + return "N/A" + + # Step 1: Check for tier partitioning (T3: ... | T4: ...) + tier_content = raw + if "T3:" in raw or "T4:" in raw: + if target_tier == "T3": + if "T3:" in raw: + after_t3 = raw.split("T3:")[1] + tier_content = after_t3.split("| T4:")[0].strip() + else: + return "N/A" + elif target_tier == "T4": + if "T4:" in raw: + tier_content = raw.split("T4:")[1].strip() + else: + return "N/A" + + # Step 2: Handle positional sequence across attempts (e.g. '503 | 200') + if "|" in tier_content: + parts = [p.strip() for p in tier_content.split("|")] + return parts[attempt_idx] if attempt_idx < len(parts) else parts[-1] + + return tier_content.strip() + + +# OpenTelemetry attributes validated as strings +STRING_ATTRIBUTES = [ + "rpc.system.name", + "rpc.method", + "rpc.response.status_code", + "http.request.method", + "url.domain", + "url.template", + "server.address", + "error.type", +] + +# OpenTelemetry attributes validated as exact integer types +INTEGER_ATTRIBUTES = [ + "http.response.status_code", + "rpc.grpc.status_code", + "server.port", +] + + +def _assert_attribute( + span, + attr: str, + raw_expected: str | None, + target_tier: str, + attempt_idx: int = 0, + comparator=None, +): + """Universal triage helper for single-attribute validation against a matrix cell. + + This helper standardizes attribute verification across all sub-validators: + - 'N/A': Skips assertion (attribute is irrelevant or not applicable to this scenario). + - 'NOT SET': Enforces negative assertion, verifying the key is completely absent + from `span.attributes`. + - Present values: Asserts presence, then applies either a custom comparator callback + (e.g. for wildcard prefixes or integer parsing) or default strict equality. + + Args: + span: The OpenTelemetry ReadableSpan instance being evaluated. + attr: The attribute name key (e.g. 'rpc.system.name'). + raw_expected: The raw string value from the CSV row cell. + target_tier: The span tier being evaluated ('T3' or 'T4'). + attempt_idx: Zero-based attempt index for child spans (0 for root). + comparator: Optional callable `comparator(actual, expected)` for custom validation. + """ + expected = resolve_expected_value(raw_expected, target_tier, attempt_idx) + if expected == "N/A": + return + + if expected == "NOT SET": + assert attr not in span.attributes, ( + f"Attribute {attr} should NOT be set on {target_tier} attempt {attempt_idx}, " + f"found: {span.attributes.get(attr)}" + ) + return + + actual = span.attributes.get(attr) + assert actual is not None, ( + f"Attribute {attr} missing on {target_tier} attempt {attempt_idx}, " + f"expected '{expected}'" + ) + + if comparator: + comparator(actual, expected) + else: + assert actual == expected, ( + f"Attribute {attr} on {target_tier} attempt {attempt_idx}: " + f"expected '{expected}', got '{actual}'" + ) + + +def _assert_span_metadata( + span, row: dict[str, str], target_tier: str, attempt_idx: int +): + """Validates top-level span header fields (Name, Kind, and Status Code). + + Args: + span: The OpenTelemetry ReadableSpan being validated. + row: The CSV requirements dictionary for this feature. + target_tier: 'T3' (root operation) or 'T4' (child attempt). + attempt_idx: Zero-based attempt index. + """ + metadata_fields = ( + ("Span Name", span.name, "Span name"), + ("Span Kind", span.kind.name, "Span kind"), + ("Span Status", span.status.status_code.name, "Span status"), + ) + for col, actual, label in metadata_fields: + expected = resolve_expected_value(row.get(col), target_tier, attempt_idx) + if expected != "N/A": + assert actual == expected, ( + f"{label} mismatch on {target_tier} attempt {attempt_idx}: " + f"expected '{expected}', got '{actual}'" + ) + + +def _assert_string_attributes( + span, row: dict[str, str], target_tier: str, attempt_idx: int +): + """Validates string OpenTelemetry attributes with wildcard prefix support. + + Supports wildcard templates in the matrix (e.g. 'http://localhost:7469/v1beta1/echo:echo/*') + by checking `startswith` when an expected value ends with '/*'. + + Args: + span: The OpenTelemetry ReadableSpan being validated. + row: The CSV requirements dictionary for this feature. + target_tier: 'T3' or 'T4'. + attempt_idx: Zero-based attempt index. + """ + + def _match_string(actual: Any, expected: str): + if expected.endswith("/*"): + prefix = expected[:-1] + assert str(actual).startswith(prefix), ( + f"Expected attribute to start with '{prefix}', got '{actual}'" + ) + else: + assert actual == expected, f"Expected '{expected}', got '{actual}'" + + for attr in STRING_ATTRIBUTES: + _assert_attribute( + span, + attr, + row.get(attr), + target_tier, + attempt_idx, + comparator=_match_string, + ) + + +def _assert_integer_attributes( + span, row: dict[str, str], target_tier: str, attempt_idx: int +): + """Validates integer OpenTelemetry attributes (e.g. status codes, ports). + + Enforces strict integer type conversion to prevent false positives where string + values might accidentally pass comparison. + + Args: + span: The OpenTelemetry ReadableSpan being validated. + row: The CSV requirements dictionary for this feature. + target_tier: 'T3' or 'T4'. + attempt_idx: Zero-based attempt index. + """ + + def _match_int(actual: Any, expected: str): + assert actual == int(expected), f"Expected {expected}, got {actual}" + + for attr in INTEGER_ATTRIBUTES: + _assert_attribute( + span, + attr, + row.get(attr), + target_tier, + attempt_idx, + comparator=_match_int, + ) + + +def _assert_status_message( + span, row: dict[str, str], target_tier: str, attempt_idx: int +): + """Validates status.message via substring containment matching. + + Because gRPC and REST backends format error message payloads slightly differently, + the matrix specifies key error phrases (e.g. 'Persistent outage') that must appear + inside the captured `status.message` attribute. + + Args: + span: The OpenTelemetry ReadableSpan being validated. + row: The CSV requirements dictionary for this feature. + target_tier: 'T3' or 'T4'. + attempt_idx: Zero-based attempt index. + """ + + def _match_message(actual: Any, expected: str): + assert expected in str(actual), ( + f"Expected '{expected}' in status.message '{actual}'" + ) + + _assert_attribute( + span, + "status.message", + row.get("status.message"), + target_tier, + attempt_idx, + comparator=_match_message, + ) + + +def _assert_resend_count(span, row: dict[str, str], target_tier: str, attempt_idx: int): + """Validates retry resend counts across HTTP and gRPC attribute variations. + + Protocol Discrepancies Handled: + - HTTP / REST spans record resend counts under `http.request.resend_count`. + - gRPC spans record resend counts under `gcp.grpc.resend_count`. + + Dynamic Index Token ('attempt_index'): + - Attempt 0 (initial request): Resend count must be NOT SET (initial call is not a retry). + - Attempt N (N > 0): Resend count must equal integer N. + + Args: + span: The OpenTelemetry ReadableSpan being validated. + row: The CSV requirements dictionary for this feature. + target_tier: 'T3' or 'T4'. + attempt_idx: Zero-based attempt index. + """ + expected_resend = resolve_expected_value( + row.get("resend_count"), target_tier, attempt_idx + ) + if expected_resend == "attempt_index": + expected_resend = "NOT SET" if attempt_idx == 0 else str(attempt_idx) + + if expected_resend == "NOT SET": + assert "http.request.resend_count" not in span.attributes + assert "gcp.grpc.resend_count" not in span.attributes + elif expected_resend != "N/A": + actual_resend = span.attributes.get( + "http.request.resend_count" + ) or span.attributes.get("gcp.grpc.resend_count") + assert actual_resend is not None, ( + f"Resend count missing on {target_tier} attempt {attempt_idx}, " + f"expected {expected_resend}" + ) + assert actual_resend == int(expected_resend), ( + f"Resend count on {target_tier} attempt {attempt_idx}: " + f"expected {expected_resend}, got {actual_resend}" + ) + + +def assert_span_matches_row( + span, row: dict[str, str], target_tier: str, attempt_idx: int = 0 +): + """Master validator delegating span verification across specialized sub-validators. + + Coordinates verification of: + 1. Top-level Span Metadata (Name, Kind, Status) + 2. String Attributes (System, Method, URL, Error Type) + 3. Integer Attributes (HTTP / gRPC Status Codes, Port) + 4. Status Error Message (Substring containment) + 5. Retry Resend Count (Protocol-aware retry progression) + + Args: + span: The OpenTelemetry ReadableSpan instance being checked. + row: The CSV requirements dictionary for this feature. + target_tier: 'T3' (logical operation) or 'T4' (wire attempt). + attempt_idx: Zero-based attempt index (defaults to 0). + """ + _assert_span_metadata(span, row, target_tier, attempt_idx) + _assert_string_attributes(span, row, target_tier, attempt_idx) + _assert_integer_attributes(span, row, target_tier, attempt_idx) + _assert_status_message(span, row, target_tier, attempt_idx) + _assert_resend_count(span, row, target_tier, attempt_idx) + + +# --------------------------------------------------------------------------- +# Parametrized Test Runner (22 Features) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "feature_id", + list(FEATURE_MATRIX.keys()), + ids=[ + f"{fid}_{FEATURE_MATRIX[fid]['Feature Name'].replace(' ', '_')}" + for fid in FEATURE_MATRIX + ], +) +def test_feature(feature_id: str, span_exporter, monkeypatch): + """Executes a single observability scenario and validates 1-to-1 against matrix specifications. + + Test Lifecycle: + --------------- + 1. Execute Scenario: + Invokes `execute_scenario` to dispatch unary or retry requests to the live Showcase server. + 2. Capture Spans: + Retrieves all finished spans from the in-memory exporter and archives them into + `RAW_SPANS_CATALOG` for diagnostic output. + 3. Handle 'Tracing Off': + If the row specifies 'Span Count = 0', asserts that zero spans were produced. + 4. Span Partitioning: + Partitions captured spans into root spans (`parent is None`, representing T3) + and child spans (`parent is not None`, representing T4). + 5. Evaluate T3 Root Span: + Verifies that exactly 1 root span was emitted (if required by Tier) and validates + its metadata and attributes. + 6. Evaluate T4 Child Spans: + Verifies child span cardinality, evaluates each attempt sequentially against + positional expectations, and validates the parent-child span ID linkage invariant. + + Args: + feature_id: Unique matrix identifier (e.g. 'grpc_happy_path', 'rest_retry_recovery'). + span_exporter: Isolated test fixture yielding (exporter, provider). + monkeypatch: Pytest environment variable isolation fixture. + """ + row = FEATURE_MATRIX[feature_id] + exporter, provider = span_exporter + + # Phase 1: Execute physical scenario against Showcase daemon + execute_scenario(row["Scenario"], row["Transport"], provider, exporter, monkeypatch) + spans = exporter.get_finished_spans() + + # Phase 2: Archive raw spans in memory for end-of-session JSON diagnostic dump + RAW_SPANS_CATALOG[feature_id] = [json.loads(s.to_json()) for s in spans] + + # Phase 3: Verify Tracing Off scenarios emit zero telemetry + if row["Span Count"] == "0": + assert len(spans) == 0, f"Expected 0 spans for {feature_id}, found {len(spans)}" + return + + # Phase 4: Partition captured spans into Root (T3) and Child (T4) attempts + t3_spans = [s for s in spans if s.parent is None] + t4_spans = [s for s in spans if s.parent is not None] + + tier = row["Tier"] + + # Phase 5: Evaluate Root T3 Span (if applicable) + if tier in ("T3", "T3 + T4"): + assert len(t3_spans) == 1, f"Expected 1 T3 root span, got {len(t3_spans)}" + assert_span_matches_row(t3_spans[0], row, target_tier="T3", attempt_idx=0) + + # Phase 6: Evaluate Child T4 Spans (if applicable) + if tier in ("T4", "T3 + T4"): + expected_count = row["Span Count"] + if expected_count.isdigit(): + expected_t4_count = ( + int(expected_count) if tier == "T4" else int(expected_count) - 1 + ) + assert len(t4_spans) == expected_t4_count, ( + f"Expected {expected_t4_count} T4 child spans, got {len(t4_spans)}" + ) + else: + # Dynamic count for retries exhausted (N attempts + 1 root >= 2 children) + assert len(t4_spans) >= 2, ( + f"Expected >=2 T4 child spans, got {len(t4_spans)}" + ) + + for idx, child in enumerate(t4_spans): + assert_span_matches_row(child, row, target_tier="T4", attempt_idx=idx) + # Hierarchy Invariant: child span must point to root T3 span ID as its parent + if t3_spans: + assert child.parent.span_id == t3_spans[0].context.span_id + + +# --------------------------------------------------------------------------- +# Session Teardown: Raw Spans Archival +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="session", autouse=True) +def dump_raw_spans(): + """Serializes all captured raw spans to raw_spans_output.json upon session completion. + + This autouse session fixture runs after all tests complete, writing an immutable + diagnostic artifact showing every span's exact attributes, timing, and hierarchy + ready for upload by CI. + """ + yield + if RAW_SPANS_CATALOG: + output_path = Path(__file__).parent / "raw_spans_output.json" + with open(output_path, "w", encoding="utf-8") as f: + json.dump(RAW_SPANS_CATALOG, f, indent=2) diff --git a/packages/gapic-generator/tests/system/test_tracing.py b/packages/gapic-generator/tests/system/test_tracing.py new file mode 100644 index 000000000000..132fbac9aca7 --- /dev/null +++ b/packages/gapic-generator/tests/system/test_tracing.py @@ -0,0 +1,237 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from unittest import mock + +import grpc +import pytest + +try: + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + + HAS_OPENTELEMETRY = True +except ImportError: + HAS_OPENTELEMETRY = False + +if not HAS_OPENTELEMETRY: + pytest.skip("OpenTelemetry is not installed", allow_module_level=True) + +try: + from google.api_core import _observability + + HAS_TIER3_TRACING = hasattr(_observability, "_TraceContext") +except ImportError: + HAS_TIER3_TRACING = False + +if not HAS_TIER3_TRACING: + pytest.skip( + "Installed google-api-core lacks Tier 3 OpenTelemetry tracing", + allow_module_level=True, + ) + +from google import showcase +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 + +try: + from .conftest import construct_client +except (ImportError, ValueError): + from conftest import construct_client + + +@pytest.fixture +def span_exporter(): + """Provides an isolated InMemorySpanExporter and TracerProvider for test assertions.""" + exporter = InMemorySpanExporter() + provider = TracerProvider() + processor = SimpleSpanProcessor(exporter) + provider.add_span_processor(processor) + + yield exporter, provider + + exporter.clear() + + +@pytest.fixture +def otel_echo_client(span_exporter, use_mtls): + """Constructs an EchoClient wired with an in-memory TracerProvider.""" + exporter, provider = span_exporter + options = ClientOptions( + tracer_provider=provider, + ) + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"} + ): + client = construct_client( + EchoClient, + use_mtls, + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + yield client, exporter + + +def test_tracing_disabled_default(span_exporter, use_mtls): + """Verifies that default client options emit zero spans (zero overhead guarantee). + + Ensures that without setting GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED=true, + even if an ambient TracerProvider is active, zero spans are recorded and no + tracing overhead is incurred. Also verifies that passing tracer_provider without + the environment variable fails fast by raising FeatureGatingError. + """ + exporter, provider = span_exporter + + # Providing a tracer_provider without enabling the experimental env var fails fast + options_with_provider = ClientOptions( + tracer_provider=provider, + ) + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "false"} + ): + with pytest.raises(FeatureGatingError): + construct_client( + EchoClient, + use_mtls, + client_options=options_with_provider, + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Default client options emit zero spans + options = ClientOptions() + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "false"} + ): + client = construct_client( + EchoClient, + use_mtls, + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + + response = client.echo(showcase.EchoRequest(content="no tracing")) + assert response.content == "no tracing" + + # Zero spans must be emitted when tracing is disabled + spans = exporter.get_finished_spans() + assert len(spans) == 0 + + +def test_custom_tracer_provider(use_mtls): + """Verifies that spans are emitted exclusively to the injected custom TracerProvider. + + Ensures strict isolation of trace data: when a client is configured with a + custom `TracerProvider`, generated RPC spans must be routed solely to that + provider's exporters and never leak into the ambient/global `TracerProvider`. + + Configures an ambient global `TracerProvider` with `global_exporter`, while + configuring the client with `custom_provider` and `custom_exporter`. After + executing an RPC, the test asserts that `custom_exporter` captured the span + while `global_exporter` recorded zero spans. + """ + custom_exporter = InMemorySpanExporter() + custom_provider = TracerProvider() + custom_provider.add_span_processor(SimpleSpanProcessor(custom_exporter)) + + global_exporter = InMemorySpanExporter() + global_provider = TracerProvider() + global_provider.add_span_processor(SimpleSpanProcessor(global_exporter)) + + # Temporarily set the ambient global tracer provider + original_provider = trace.get_tracer_provider() + trace.set_tracer_provider(global_provider) + try: + options = ClientOptions( + tracer_provider=custom_provider, + ) + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"} + ): + client = construct_client( + EchoClient, + use_mtls, + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + + response = client.echo(showcase.EchoRequest(content="isolated trace")) + assert response.content == "isolated trace" + + custom_spans = custom_exporter.get_finished_spans() + assert len(custom_spans) == 2 + global_spans = global_exporter.get_finished_spans() + assert len(global_spans) == 0 + finally: + trace.set_tracer_provider(original_provider) + + +def test_direct_client_initialization_tracing(span_exporter): + """Verifies end-to-end trace injection via direct EchoClient instantiation. + + Validates the template wiring in `client.py.j2` directly. In system test + harnesses, `construct_client` often creates the transport instance manually, + which bypasses `client.py`'s `if not transport_provided:` branch. This test + instantiates `EchoClient(client_options=...)` directly to prove that the client + resolves `_observability.get_otel_interceptor` and passes it to `EchoGrpcTransport`. + + Constructs `EchoClient` without a pre-instantiated transport. Patches + `EchoGrpcTransport.create_channel` solely to target the local insecure Showcase + endpoint (`localhost:7469`). Executes `client.echo()` and asserts span generation. + """ + exporter, provider = span_exporter + options = ClientOptions( + tracer_provider=provider, + ) + + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"} + ): + with mock.patch.object( + EchoClient.get_transport_class("grpc"), + "create_channel", + side_effect=lambda host, **kwargs: grpc.insecure_channel("localhost:7469"), + ): + # Client constructs the transport and wires interceptors itself + client = EchoClient( + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + response = client.echo(showcase.EchoRequest(content="direct client wiring")) + assert response.content == "direct client wiring" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + for span in spans: + assert span.name == "google.showcase.v1beta1.Echo/Echo" + assert span.attributes.get("rpc.system.name") == "grpc" + + +def test_env_var_opt_in(otel_echo_client): + """Verifies that setting the environment variable enables tracing without tracing_enabled=True.""" + client, exporter = otel_echo_client + + response = client.echo(showcase.EchoRequest(content="env opt in")) + assert response.content == "env opt in" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + for span in spans: + assert span.name == "google.showcase.v1beta1.Echo/Echo" diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 2d8c50acbfa9..a7a1dd5d2a89 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -42,8 +42,9 @@ def is_otel_capabilities_enabled( """Checks if OTel capabilities are enabled and installed. Args: - client_options: The client options object or dictionary. - env_var: The environment variable to check for enablement. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options + object or dictionary. + env_var (str): The environment variable to check for enablement. Returns: bool: True if enabled and installed, False otherwise. @@ -71,7 +72,8 @@ def _extract_endpoint_attributes( """Extracts server.address, server.port (if non-default), and url.domain from client options if present. Args: - client_options: The client options object or dictionary. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options + object or dictionary. Returns: dict[str, Any]: A dictionary containing url.domain and, if an api_endpoint is configured, @@ -120,7 +122,8 @@ def _make_grpc_client_request_hook( """Creates an OpenTelemetry gRPC client request hook with optional endpoint attributes. Args: - endpoint_attrs: Optional static endpoint attributes to attach to every span. + endpoint_attrs (Optional[dict[str, Any]]): Optional static endpoint attributes to attach + to every span. Returns: Callable[[Any, Any], None]: The request hook callback. @@ -182,10 +185,10 @@ def _grpc_client_response_hook(span: Any, response: Any) -> None: modern ``rpc.response.status_code`` in future releases, this hook can be retired. Args: - span: The OpenTelemetry span. - response: The gRPC response object or details. + span (Optional[Any]): The OpenTelemetry span. + response (Any): The gRPC response object or details. """ - if not span.is_recording(): + if span is None or not getattr(span, "is_recording", lambda: False)(): return # Guard against upstream async calls that invoke this hook on failures. @@ -206,7 +209,8 @@ def _get_tracer_provider( """Extracts the OpenTelemetry tracer provider from client options if present. Args: - client_options: The client options object or dictionary. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options + object or dictionary. Returns: opentelemetry.trace.TracerProvider | None: The tracer provider if present, @@ -225,8 +229,8 @@ def get_otel_interceptor( """Returns an interceptor callable that wraps a sync gRPC channel with OpenTelemetry tracing. Args: - client_options: The client options object or dictionary used for feature gating - and extracting the tracer provider. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options + object or dictionary used for feature gating and extracting the tracer provider. Returns: Callable[[grpc.Channel], grpc.Channel] | None: An interceptor callable if OpenTelemetry @@ -249,6 +253,7 @@ def get_otel_interceptor( def otel_interceptor(channel: grpc.Channel) -> grpc.Channel: return otel_grpc.intercept_channel(channel, interceptor) + otel_interceptor._is_otel_interceptor = True # type: ignore[attr-defined] return otel_interceptor @@ -258,8 +263,8 @@ def get_otel_async_interceptor( """Returns async gRPC client interceptors for OpenTelemetry tracing. Args: - client_options: The client options object or dictionary used for feature gating - and extracting the tracer provider. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): The client options + object or dictionary used for feature gating and extracting the tracer provider. Returns: Sequence[grpc.aio.ClientInterceptor] | None: Instantiated OpenTelemetry async @@ -279,3 +284,309 @@ def get_otel_async_interceptor( request_hook=request_hook, response_hook=_grpc_client_response_hook, ) + + +_TRACE_CONTEXT_PROPAGATOR: Any = None + + +class _TraceContext: + """Context manager for tracing an HTTP wire request with OpenTelemetry. + + Manages span creation, semantic attribute attachment, W3C traceparent injection, + and automatic error recording on failure without suppressing caller exceptions. + + Supports two calling conventions: + - Pass a single `request` object (such as `requests.PreparedRequest`). + - Pass explicit keyword arguments (`method`, `url`, `headers`, `body`, `client_options`). + """ + + @staticmethod + def _get_trace_context_propagator() -> Any: + global _TRACE_CONTEXT_PROPAGATOR + if _TRACE_CONTEXT_PROPAGATOR is None: + from opentelemetry.trace.propagation.tracecontext import ( # type: ignore[import-not-found] + TraceContextTextMapPropagator, + ) + + _TRACE_CONTEXT_PROPAGATOR = TraceContextTextMapPropagator() + return _TRACE_CONTEXT_PROPAGATOR + + # The HTTP tracing context manager deliberately supports two distinct invocation styles: + # 1. Bundled Request Object: `trace_http_request(request, ...)` + # Used when callers already possess an HTTP request instance (such as + # requests.PreparedRequest or urllib.request.Request) with `.method`, `.url`, etc. + # 2. Unpacked Keyword Arguments: `trace_http_request(method=..., url=..., headers=..., body=...)` + # Used by generated GAPIC REST transports (_shared_macros.j2). + # In GAPIC templates, requests are assembled from local strings and dictionaries before + # hitting the session. Supporting keyword arguments avoids the CPU and memory overhead + # of instantiating a throwaway dummy request object on every single RPC execution. + @staticmethod + def _build_http_span_attributes( + request: Any = None, + *, + method: str | None = None, + url: str | None = None, + url_template: str | None = None, + headers: dict[str, Any] | None = None, + body: Any = None, + client_options: ClientOptions | dict[str, Any] | None = None, + ) -> tuple[str, dict[str, Any], Any]: + """Extract span name, semantic attributes dictionary, and resolved headers. + + Supports two calling conventions: + - Pass a single `request` object (such as `requests.PreparedRequest`). + - Pass explicit keyword arguments (`method`, `url`, `headers`, `body`, `client_options`). + + Returns: + tuple[str, dict[str, Any], Any]: A tuple of (span_name, attributes, resolved_headers). + """ + if request is not None: + resolved_method = getattr(request, "method", "HTTP") or "HTTP" + resolved_url = getattr(request, "url", "") or "" + resolved_headers = getattr(request, "headers", None) + resolved_body = getattr(request, "body", None) + else: + resolved_method = method or "HTTP" + resolved_url = url or "" + resolved_headers = headers + resolved_body = body + + resolved_method = resolved_method.upper() + endpoint_attrs = _extract_endpoint_attributes(client_options) + + server_address = endpoint_attrs.get("server.address") + server_port = endpoint_attrs.get("server.port") + if not server_address and resolved_url: + try: + parsed = urllib.parse.urlsplit(resolved_url) + server_address = parsed.hostname + if not server_port and parsed.port: + server_port = parsed.port + except Exception: # Fail-open on malformed URL parsing + pass + + span_name = resolved_method + span_attributes: dict[str, Any] = { + "http.request.method": resolved_method, + "server.address": server_address or "", + "server.port": server_port or 443, + "url.domain": endpoint_attrs.get("url.domain", "googleapis.com"), + } + if url_template: + span_attributes["url.template"] = url_template + if resolved_url: + span_attributes["url.full"] = resolved_url + + if resolved_body is not None and isinstance(resolved_body, (bytes, str)): + span_attributes["http.request.body.size"] = len(resolved_body) + + return span_name, span_attributes, resolved_headers + + def __init__( + self, + request: Any = None, + *, + method: str | None = None, + url: str | None = None, + url_template: str | None = None, + headers: dict[str, Any] | None = None, + body: Any = None, + client_options: ClientOptions | dict[str, Any] | None = None, + ): + self._request = request + self._method = method + self._url = url + self._url_template = url_template + self._headers = headers + self._body = body + self._client_options = client_options + self._span: Any = None + self._cm: Any = None + + def record_response(self, response: Any) -> None: + """Record HTTP response attributes on the active span. + + Args: + response (Any): The HTTP response object (e.g. requests.Response). + """ + span = self._span + if span is None or not hasattr(span, "set_attribute"): + return + + try: + from opentelemetry.trace.status import ( # type: ignore[import-not-found] + Status, + StatusCode, + ) + + status_code = getattr( + response, "status_code", getattr(response, "status", None) + ) + if status_code is not None: + span.set_attribute("http.response.status_code", int(status_code)) + if int(status_code) >= 400: + span.set_status(Status(StatusCode.ERROR)) + else: + span.set_status(Status(StatusCode.OK)) + + headers = getattr(response, "headers", None) + if headers and "Content-Length" in headers: + try: + span.set_attribute( + "http.response.body.size", int(headers["Content-Length"]) + ) + except (ValueError, TypeError): + pass + elif hasattr(response, "_content") and response._content is not None: + try: + span.set_attribute( + "http.response.body.size", len(response._content) + ) + except Exception: + pass + except Exception: + pass + + # Alias for method name parity with Daniel's suggested convention + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + """Record an HTTP error/exception on the active span. + + Args: + exc (Optional[BaseException]): The exception raised during dispatch. + """ + span = self._span + if span is None or exc is None: + return + + try: + from opentelemetry.trace.status import ( # type: ignore[import-not-found] + Status, + StatusCode, + ) + + if hasattr(span, "record_exception"): + span.record_exception(exc) + if hasattr(span, "set_status"): + span.set_status(Status(StatusCode.ERROR)) + if hasattr(span, "set_attribute"): + status_code = getattr(exc, "code", None) or getattr( + exc, "status_code", None + ) + if status_code: + span.set_attribute("error.type", str(status_code)) + else: + span.set_attribute("error.type", exc.__class__.__name__) + msg = str(exc) + if msg: + span.set_attribute("status.message", msg) + except Exception: # Fail-open on error attribute extraction failure + pass + + # Alias for method name parity with Daniel's suggested convention + record_http_error = record_error + + def __enter__(self) -> "_TraceContext": + if not is_otel_capabilities_enabled(self._client_options): + return self + + try: + from opentelemetry import trace + + tracer_provider = _get_tracer_provider(self._client_options) + if tracer_provider is not None: + tracer = tracer_provider.get_tracer("google.api_core") + else: + tracer = trace.get_tracer("google.api_core") + + span_name, span_attributes, resolved_headers = ( + self._build_http_span_attributes( + self._request, + method=self._method, + url=self._url, + url_template=self._url_template, + headers=self._headers, + body=self._body, + client_options=self._client_options, + ) + ) + + self._cm = tracer.start_as_current_span( + span_name, + kind=trace.SpanKind.CLIENT, + attributes=span_attributes, + ) + self._span = self._cm.__enter__() + + if resolved_headers is not None and hasattr( + resolved_headers, "__setitem__" + ): + try: + self._get_trace_context_propagator().inject(resolved_headers) + except Exception: # Fail-open on header injection failure + pass + + return self + except Exception: + # Fail-open: telemetry failures must never disrupt core RPC execution + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + if self._cm is not None: + try: + if exc_val is not None and self._span is not None: + self.record_error(exc_val) + finally: + self._cm.__exit__(exc_type, exc_val, exc_tb) + # Always return None so caller exceptions are never suppressed + return None + + +def trace_http_request( + request: Any = None, + *, + method: str | None = None, + url: str | None = None, + url_template: str | None = None, + headers: dict[str, Any] | None = None, + body: Any = None, + client_options: ClientOptions | dict[str, Any] | None = None, +) -> _TraceContext: + """Context manager factory for tracing an HTTP wire request with OpenTelemetry. + + Manages span creation, semantic attribute attachment, W3C traceparent injection, + and automatic error recording on failure without suppressing caller exceptions. + + Supports two calling conventions: + 1. Bundled Request Object: `trace_http_request(request, ...)` + Used when callers already possess an HTTP request instance (such as + `requests.PreparedRequest` or `urllib.request.Request`) with `.method`, `.url`, etc. + 2. Unpacked Keyword Arguments: `trace_http_request(method=..., url=..., headers=..., body=...)` + Used by generated GAPIC REST transports (_shared_macros.j2). + In GAPIC templates, requests are assembled from local strings and dictionaries before + hitting the session. Supporting keyword arguments avoids the CPU and memory overhead + of instantiating a throwaway dummy request object on every single RPC execution. + + Args: + request (Optional[Any]): Bundled HTTP request object with .method, .url, .headers. + method (Optional[str]): Explicit HTTP method (e.g. "GET", "POST"). + url (Optional[str]): Fully qualified request URL. + url_template (Optional[str]): Parameterized path template for url.template. + headers (Optional[dict[str, Any]]): Mutable dictionary of request headers. + body (Optional[Any]): Request payload (used for body size calculation). + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): Client options + containing custom endpoint and tracer provider configurations. + + Returns: + _TraceContext: An active context manager managing the HTTP client span. + """ + return _TraceContext( + request=request, + method=method, + url=url, + url_template=url_template, + headers=headers, + body=body, + client_options=client_options, + ) diff --git a/packages/google-api-core/google/api_core/gapic_v1/method.py b/packages/google-api-core/google/api_core/gapic_v1/method.py index 9b10b0392acf..b12a0e126a60 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method.py @@ -216,11 +216,25 @@ def _extract_error_attributes(exc: Optional[Exception]) -> dict[str, Any]: reason = getattr(source, "reason", None) if reason: attrs["error.type"] = reason + else: + # Fallback per OpenTelemetry Semantic Conventions: every failed span should record + # a low-cardinality error.type. Use canonical status code name or exception class name. + status_code = _extract_status_code(target_exc) + attrs["error.type"] = status_code or target_exc.__class__.__name__ metadata = getattr(source, "metadata", None) if metadata: for k, v in metadata.items(): attrs[f"gcp.errors.metadata.{k}"] = str(v) + # 5. Extract human-readable error description for cross-language PRD parity + message = getattr(target_exc, "message", None) + if not message and hasattr(target_exc, "details") and callable(target_exc.details): + message = target_exc.details() + if not message and isinstance(target_exc, Exception): + message = str(target_exc) + if message: + attrs["status.message"] = str(message) + return attrs @@ -254,7 +268,7 @@ class _GapicCallable(object): client_info (Optional[google.api_core.gapic_v1.client_info.ClientInfo]): Client information used for metadata headers. Defaults to None. kind (str): The transport kind for the RPC method. Defaults to "grpc". - Allowed values for OpenTelemetry method tracing are "grpc" and "grpc_asyncio". + Allowed values for OpenTelemetry method tracing are "grpc" and "rest". """ def __init__( @@ -291,7 +305,7 @@ def __init__( self._start_span_fn = None if ( not is_streaming - and kind == "grpc" + and kind in ("grpc", "rest") and method_name is not None and _observability.is_otel_capabilities_enabled(client_options) ): @@ -309,8 +323,9 @@ def __init__( tracer = trace.get_tracer("google.api_core") span_name, _, _ = _extract_rpc_identity(method_name) + is_rest = kind in ("rest", "rest_asyncio") span_attributes = { - "rpc.system.name": "grpc", + "rpc.system.name": "http" if is_rest else "grpc", "rpc.method": span_name, } self._start_span_fn = functools.partial( @@ -358,14 +373,16 @@ def __call__( elif self._default_metadata: kwargs["metadata"] = self._default_metadata - if self._compression is not None: + if compression is not None: kwargs["compression"] = compression span_cm = contextlib.nullcontext() if self._start_span_fn is not None: try: span_cm = self._start_span_fn() - except Exception: + except ( + Exception + ): # Fail-open: proceed without span if tracing initialization fails span_cm = contextlib.nullcontext() with span_cm as span: @@ -489,8 +506,7 @@ def get_topic(name, timeout=None): is_streaming (bool): Whether the RPC method is streaming. Defaults to False. Streaming methods are currently gated and do not generate Tier 3 spans. kind (str): The transport kind for the RPC method. Defaults to "grpc". - Non-gRPC transports (e.g. "rest") are currently gated and do not generate - Tier 3 method spans. + Allowed values for OpenTelemetry method tracing are "grpc" and "rest". Returns: Callable: A new callable that takes optional ``retry``, ``timeout``, diff --git a/packages/google-api-core/google/api_core/gapic_v1/method_async.py b/packages/google-api-core/google/api_core/gapic_v1/method_async.py index d361bf9f961f..e550b4228745 100644 --- a/packages/google-api-core/google/api_core/gapic_v1/method_async.py +++ b/packages/google-api-core/google/api_core/gapic_v1/method_async.py @@ -11,25 +11,200 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""AsyncIO helpers for wrapping gRPC methods with common functionality. +"""AsyncIO helpers for wrapping gRPC and REST methods with common functionality. This is used by gapic clients to provide common error mapping, retry, timeout, -compression, pagination, and long-running operations to gRPC methods. +compression, pagination, and long-running operations to methods. """ +import asyncio +import contextlib import functools +import inspect -from google.api_core import grpc_helpers_async +from google.api_core import _observability, grpc_helpers_async from google.api_core.gapic_v1 import client_info +from google.api_core.gapic_v1.client_info import METRICS_METADATA_KEY + +# Retain _GapicCallable import for backward compatibility with external packages from google.api_core.gapic_v1.method import ( # noqa: F401 DEFAULT, USE_DEFAULT_METADATA, + _apply_decorators, + _deduplicate_metadata_tokens, + _extract_error_attributes, + _extract_metrics_header, + _extract_rpc_identity, + _extract_status_code, _GapicCallable, ) +from google.api_core.timeout import TimeToDeadlineTimeout _DEFAULT_ASYNC_TRANSPORT_KIND = "grpc_asyncio" +class _AsyncGapicCallable(object): + """Async callable object that wraps an async RPC method with retry, timeout, metadata, and tracing. + + Args: + target (Callable): The low-level async RPC method. + retry (Optional[google.api_core.retry_async.AsyncRetry]): The default retry for the + callable. If ``None``, this callable will not retry by default. + timeout (Optional[Union[google.api_core.timeout.Timeout, float]]): The default timeout for the + callable. If ``None``, this callable will not specify a timeout argument to the + low-level RPC method. + compression (Optional[grpc.Compression]): The default compression for the callable. + If ``None``, this callable will not specify a compression argument to the low-level + RPC method. + metadata (Optional[Sequence[Tuple[str, str]]]): Additional metadata that is + provided to the RPC method on every invocation. This is merged with + any metadata specified during invocation. If ``None``, no + additional metadata will be passed to the RPC method. + client_options (Optional[google.api_core.client_options.ClientOptions]): + Client options used to configure client-level behavior, such as + custom OpenTelemetry tracer providers. Defaults to None. + method_name (Optional[str]): The optional explicit full RPC method name + (e.g. "/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion"). + is_streaming (bool): Whether the RPC method is streaming. Defaults to False. + Note: Streaming methods do not currently generate Tier 3 observability spans. + client_info (Optional[google.api_core.gapic_v1.client_info.ClientInfo]): + Client information used for metadata headers. Defaults to None. + kind (str): The transport kind for the RPC method. Defaults to "grpc_asyncio". + Allowed values for OpenTelemetry method tracing are "grpc_asyncio" and "rest_asyncio". + """ + + def __init__( + self, + target, + retry, + timeout, + compression, + metadata=None, + client_options=None, + method_name=None, + is_streaming=False, + client_info=None, + kind=_DEFAULT_ASYNC_TRANSPORT_KIND, + ): + self._target = target + self._retry = retry + self._timeout = timeout + self._compression = compression + + # Pre-extract the x-goog-api-client header from the initialized metadata. + self._x_goog_api_client, remaining = _extract_metrics_header(metadata) + self._static_metadata = tuple(remaining) + if self._x_goog_api_client: + self._default_metadata = ( + (METRICS_METADATA_KEY, self._x_goog_api_client), + *self._static_metadata, + ) + else: + self._default_metadata = self._static_metadata + + # Configure the OpenTelemetry span factory once at initialization. + self._start_span_fn = None + if ( + not is_streaming + and kind in ("grpc_asyncio", "rest_asyncio") + and method_name is not None + and _observability.is_otel_capabilities_enabled(client_options) + ): + try: + from opentelemetry import trace + + tracer_provider = None + if isinstance(client_options, dict): + tracer_provider = client_options.get("tracer_provider") + elif client_options is not None: + tracer_provider = getattr(client_options, "tracer_provider", None) + if tracer_provider is not None: + tracer = tracer_provider.get_tracer("google.api_core") + else: + tracer = trace.get_tracer("google.api_core") + + span_name, _, _ = _extract_rpc_identity(method_name) + is_rest = kind in ("rest", "rest_asyncio") + span_attributes = { + "rpc.system.name": "http" if is_rest else "grpc", + "rpc.method": span_name, + } + self._start_span_fn = functools.partial( + tracer.start_as_current_span, + span_name, + kind=trace.SpanKind.CLIENT, + attributes=span_attributes, + ) + except (ImportError, AttributeError, TypeError): + # Gracefully disable tracing if OpenTelemetry or custom provider fails + self._start_span_fn = None + + async def __call__( + self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs + ): + """Invoke the low-level async RPC with retry, timeout, compression, and metadata.""" + if retry is DEFAULT: + retry = self._retry + + if timeout is DEFAULT: + timeout = self._timeout + + if compression is DEFAULT: + compression = self._compression + + if isinstance(timeout, (int, float)): + timeout = TimeToDeadlineTimeout(timeout=timeout) + + # Apply all applicable decorators. + wrapped_func = _apply_decorators(self._target, [retry, timeout]) + + if user_metadata := kwargs.get("metadata"): + # Add the user agent metadata to the call. + final_metadata = list(self._static_metadata) + user_x_goog, remaining = _extract_metrics_header(user_metadata) + + merged_header = _deduplicate_metadata_tokens( + self._x_goog_api_client, user_x_goog + ) + if merged_header: + final_metadata.append((METRICS_METADATA_KEY, merged_header)) + final_metadata.extend(remaining) + kwargs["metadata"] = final_metadata + elif self._default_metadata: + kwargs["metadata"] = self._default_metadata + + if compression is not None: + kwargs["compression"] = compression + + span_cm = contextlib.nullcontext() + if self._start_span_fn is not None: + try: + span_cm = self._start_span_fn() + except ( + Exception + ): # Fail-open: proceed without span if tracing initialization fails + span_cm = contextlib.nullcontext() + + with span_cm as span: + try: + res = wrapped_func(*args, **kwargs) + if inspect.isawaitable(res): + result = await res + else: + result = res + if span is not None and hasattr(span, "set_attribute"): + span.set_attribute("rpc.response.status_code", "OK") + return result + except (Exception, asyncio.CancelledError) as exc: + if span is not None and hasattr(span, "set_attribute"): + span.set_attribute( + "rpc.response.status_code", _extract_status_code(exc) + ) + for k, v in _extract_error_attributes(exc).items(): + span.set_attribute(k, v) + raise + + def wrap_method( func, default_retry=None, @@ -37,6 +212,10 @@ def wrap_method( default_compression=None, client_info=client_info.DEFAULT_CLIENT_INFO, kind=_DEFAULT_ASYNC_TRANSPORT_KIND, + *, + client_options=None, + method_name=None, + is_streaming=False, ): """Wrap an async RPC method with common behavior. @@ -51,11 +230,16 @@ def wrap_method( metadata = [client_info.to_grpc_metadata()] if client_info is not None else None return functools.wraps(func)( - _GapicCallable( + _AsyncGapicCallable( func, default_retry, default_timeout, default_compression, metadata=metadata, + client_options=client_options, + method_name=method_name, + is_streaming=is_streaming, + client_info=client_info, + kind=kind, ) ) diff --git a/packages/google-api-core/google/api_core/grpc_helpers_async.py b/packages/google-api-core/google/api_core/grpc_helpers_async.py index d1f897901e7a..f0cdd1905a9a 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers_async.py +++ b/packages/google-api-core/google/api_core/grpc_helpers_async.py @@ -21,7 +21,7 @@ import asyncio import functools import warnings -from typing import AsyncGenerator, Generic, Iterator, Optional, TypeVar +from typing import AsyncGenerator, Generic, Iterator, Optional, Sequence, TypeVar import grpc from grpc import aio @@ -308,6 +308,59 @@ def create_channel( ) +def apply_channel_interceptors( + channel: aio.Channel, + interceptors: Optional[Sequence[aio.ClientInterceptor]] = None, +) -> aio.Channel: + """Applies client interceptors to a gRPC AsyncIO channel. + + In grpc.aio, channels maintain internal interceptor lists + (_unary_unary_interceptors, etc.). To preserve the public API contract for + callers who supply their own pre-instantiated ``channel`` object or a custom + channel factory callable (which does not accept ``interceptors``), we attach + interceptors post-instantiation directly to the channel's interceptor lists. + + Args: + channel (aio.Channel): The async gRPC channel to intercept. + interceptors (Optional[Sequence[aio.ClientInterceptor]]): + Additional interceptors to apply to the channel. + + Returns: + aio.Channel: The channel with interceptors attached, or the original channel + if no interceptors were provided. + """ + if not interceptors: + return channel + + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_interceptors"), + ("intercept_stream_stream", "_stream_stream_interceptors"), + ) + for interceptor in interceptors: + matched = False + 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): + if interceptor not in target_list: + target_list.append(interceptor) + matched = True + elif hasattr(target_list, "append"): + target_list.append(interceptor) + matched = True + if not matched and hasattr(channel, "_unary_unary_interceptors"): + unary_interceptors = channel._unary_unary_interceptors + if isinstance(unary_interceptors, list): + if interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + elif hasattr(unary_interceptors, "append"): + unary_interceptors.append(interceptor) + + return channel + + class FakeUnaryUnaryCall(_WrappedUnaryUnaryCall): """Fake implementation for unary-unary RPCs. diff --git a/packages/google-api-core/tests/asyncio/gapic/test_method_async.py b/packages/google-api-core/tests/asyncio/gapic/test_method_async.py index e410acbdfaab..517f475a76fd 100644 --- a/packages/google-api-core/tests/asyncio/gapic/test_method_async.py +++ b/packages/google-api-core/tests/asyncio/gapic/test_method_async.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import datetime try: @@ -26,6 +27,9 @@ except ImportError: pytest.skip("No GRPC", allow_module_level=True) +from google.api_core import ( + client_options as client_options_lib, +) from google.api_core import ( exceptions, gapic_v1, @@ -274,3 +278,320 @@ async def test_wrap_method_without_wrap_errors(): await wrapped_method() method.assert_not_called() + + +@pytest.fixture(autouse=True) +def set_event_loop(): + try: + asyncio.get_running_loop() + yield + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + yield + finally: + loop.close() + asyncio.set_event_loop(None) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "kwargs,capabilities_enabled", + [ + ( + { + "method_name": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets" + }, + False, + ), + ({}, True), + ( + { + "method_name": "/google.cloud.secretmanager.v1.SecretManagerService/StreamingRead", + "is_streaming": True, + }, + True, + ), + ( + { + "method_name": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + "kind": "unsupported_transport", + }, + True, + ), + ( + { + "method_name": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + "kind": "rest", + }, + True, + ), + ( + { + "method_name": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + "kind": "grpc", + }, + True, + ), + ], + ids=[ + "disabled_by_flag", + "omitted_method_name", + "streaming_skipped", + "unsupported_kind_skipped", + "sync_rest_kind_skipped", + "sync_grpc_kind_skipped", + ], +) +async def test_wrap_method_async_otel_tracing_skips_span( + monkeypatch, kwargs, capabilities_enabled +): + """Proves that under various gating conditions, no async Tier 3 span is created.""" + mock_target = mock.AsyncMock(return_value="success") + mock_trace = mock.Mock() + + with ( + mock.patch( + "google.api_core._observability.is_otel_capabilities_enabled", + return_value=capabilities_enabled, + ), + mock.patch.dict( + "sys.modules", + { + "opentelemetry": mock.Mock(trace=mock_trace), + "opentelemetry.trace": mock_trace, + }, + ), + ): + wrapped = gapic_v1.method_async.wrap_method(mock_target, **kwargs) + result = await wrapped() + + assert result == "success" + mock_trace.get_tracer.assert_not_called() + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_enabled_success(mock_otel): + """Proves that when OpenTelemetry tracing is enabled and method_name is passed, a T3 client span is started and awaited.""" + mock_target = mock.AsyncMock(return_value="async_success") + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + default_timeout=60, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + kind="grpc_asyncio", + ) + result = await wrapped() + + assert result == "async_success" + mock_otel.tracer.start_as_current_span.assert_called_once_with( + "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + kind="CLIENT", + attributes={ + "rpc.system.name": "grpc", + "rpc.method": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + }, + ) + mock_otel.span.set_attribute.assert_called_with("rpc.response.status_code", "OK") + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_enabled_rest_asyncio(mock_otel): + """Proves that when kind is 'rest_asyncio', a T3 client span is started.""" + mock_target = mock.AsyncMock(return_value="rest_success") + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + default_timeout=60, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + kind="rest_asyncio", + ) + result = await wrapped() + + assert result == "rest_success" + mock_otel.tracer.start_as_current_span.assert_called_once_with( + "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + kind="CLIENT", + attributes={ + "rpc.system.name": "http", + "rpc.method": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + }, + ) + mock_otel.span.set_attribute.assert_called_with("rpc.response.status_code", "OK") + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_coroutine_duration(mock_otel): + """Proves that the span remains active across asynchronous awaits and closes only after completion.""" + span_open_during_call = False + + async def delayed_target(*args, **kwargs): + nonlocal span_open_during_call + span_open_during_call = ( + mock_otel.tracer.start_as_current_span.return_value.__enter__.called + and not mock_otel.tracer.start_as_current_span.return_value.__exit__.called + ) + await asyncio.sleep(0.01) + return "delayed_result" + + wrapped = gapic_v1.method_async.wrap_method( + delayed_target, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + ) + result = await wrapped() + + assert result == "delayed_result" + assert span_open_during_call is True + assert mock_otel.tracer.start_as_current_span.return_value.__exit__.called is True + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_custom_client_options(mock_otel): + """Proves that providing client_options with a custom tracer_provider uses that provider.""" + mock_target = mock.AsyncMock(return_value="success") + + mock_provider = mock.Mock() + mock_provider.get_tracer.return_value = mock_otel.tracer + + client_options = client_options_lib.ClientOptions(tracer_provider=mock_provider) + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + client_options=client_options, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + ) + result = await wrapped() + + assert result == "success" + mock_provider.get_tracer.assert_called_once_with("google.api_core") + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_dict_client_options(mock_otel): + """Proves that providing a dict with tracer_provider uses that provider.""" + mock_target = mock.AsyncMock(return_value="success") + + mock_provider = mock.Mock() + mock_provider.get_tracer.return_value = mock_otel.tracer + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + client_options={"tracer_provider": mock_provider}, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + ) + result = await wrapped() + + assert result == "success" + mock_provider.get_tracer.assert_called_once_with("google.api_core") + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_enabled_error(mock_otel): + """Proves that on async error, status code and error attributes are recorded and exception is raised.""" + error = exceptions.NotFound("Secret not found") + mock_target = mock.AsyncMock(side_effect=error) + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/GetSecret", + ) + + with pytest.raises(exceptions.NotFound): + await wrapped() + + mock_otel.span.set_attribute.assert_any_call( + "rpc.response.status_code", "NOT_FOUND" + ) + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_records_gcp_error_attributes(mock_otel): + """Proves that GCP error attributes (domain, reason, metadata) are recorded on the span.""" + error_info = mock.Mock( + domain="googleapis.com", + reason="RESOURCE_NOT_FOUND", + metadata={"service": "secretmanager"}, + ) + error = exceptions.GoogleAPICallError("Resource not found") + error._error_info = error_info + mock_target = mock.AsyncMock(side_effect=error) + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/GetSecret", + ) + + with pytest.raises(exceptions.GoogleAPICallError): + await wrapped() + + mock_otel.span.set_attribute.assert_any_call("gcp.errors.domain", "googleapis.com") + mock_otel.span.set_attribute.assert_any_call("error.type", "RESOURCE_NOT_FOUND") + mock_otel.span.set_attribute.assert_any_call( + "gcp.errors.metadata.service", "secretmanager" + ) + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_import_error(monkeypatch): + """Proves that if opentelemetry fails to import, method execution proceeds gracefully without tracing.""" + mock_target = mock.AsyncMock(return_value="graceful_success") + + with ( + mock.patch( + "google.api_core._observability.is_otel_capabilities_enabled", + return_value=True, + ), + mock.patch.dict("sys.modules", {"opentelemetry": None}), + ): + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + ) + result = await wrapped() + + assert result == "graceful_success" + + +@pytest.mark.asyncio +async def test_wrap_method_async_otel_tracing_start_span_error_bypasses_tracing( + mock_otel, +): + """Proves that if tracer.start_as_current_span throws an exception, the call executes cleanly.""" + mock_otel.tracer.start_as_current_span.side_effect = RuntimeError("Tracing broken") + mock_target = mock.AsyncMock(return_value="resilient_success") + + wrapped = gapic_v1.method_async.wrap_method( + mock_target, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + ) + result = await wrapped() + + assert result == "resilient_success" + + +@pytest.mark.asyncio +async def test_wrap_method_async_synchronous_return_value(): + """Proves that wrap_method handles callables returning synchronous non-awaitable values.""" + + def sync_callable(*args, **kwargs): + return "synchronous_result" + + wrapped = gapic_v1.method_async.wrap_method(sync_callable, kind="rest_asyncio") + result = await wrapped(mock.sentinel.request) + assert result == "synchronous_result" + + +@pytest.mark.asyncio +async def test_invoke_wrapped_method_with_metadata_and_no_client_info(): + """Proves that wrap_method handles user metadata without client info and without metrics header.""" + fake_call = grpc_helpers_async.FakeUnaryUnaryCall() + method = mock.Mock(spec=aio.UnaryUnaryMultiCallable, return_value=fake_call) + + wrapped_method = gapic_v1.method_async.wrap_method(method, client_info=None) + + await wrapped_method(mock.sentinel.request, metadata=[("custom-header", "val")]) + + method.assert_called_once_with( + mock.sentinel.request, metadata=[("custom-header", "val")] + ) diff --git a/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py b/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py index c90c6c7bceeb..915de50297b3 100644 --- a/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py +++ b/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py @@ -17,7 +17,7 @@ from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 except ImportError: # pragma: NO COVER import mock # type: ignore -import pytest +import pytest # noqa: I202 from ..helpers import warn_deprecated_credentials_file @@ -743,3 +743,92 @@ async def test_fake_stream_unary_call(): await fake_call.wait_for_connection() response = await fake_call assert fake_call.response == response + + +def test_apply_channel_interceptors_none_or_empty(): + channel = mock.Mock() + assert grpc_helpers_async.apply_channel_interceptors(channel, None) is channel + assert grpc_helpers_async.apply_channel_interceptors(channel, []) is channel + + +def test_apply_channel_interceptors_channel_without_attr(): + channel = object() + interceptor = mock.Mock() + assert ( + grpc_helpers_async.apply_channel_interceptors(channel, [interceptor]) is channel + ) + + +def test_apply_channel_interceptors_list(): + interceptor1 = mock.Mock() + interceptor2 = mock.Mock() + channel = mock.Mock() + channel._unary_unary_interceptors = [interceptor1] + + result = grpc_helpers_async.apply_channel_interceptors( + channel, [interceptor1, interceptor2] + ) + assert result is channel + assert channel._unary_unary_interceptors == [interceptor1, interceptor2] + + +def test_apply_channel_interceptors_mock(): + interceptor1 = mock.Mock() + interceptor2 = mock.Mock() + channel = mock.Mock() + channel._unary_unary_interceptors = mock.Mock(spec=["append"]) + + result = grpc_helpers_async.apply_channel_interceptors( + channel, [interceptor1, interceptor2] + ) + assert result is channel + channel._unary_unary_interceptors.append.assert_any_call(interceptor1) + channel._unary_unary_interceptors.append.assert_any_call(interceptor2) + + +def test_apply_channel_interceptors_inner_target_not_appendable(): + channel = mock.Mock(spec=["_unary_unary_interceptors"]) + channel._unary_unary_interceptors = 123 + interceptor = mock.Mock(spec=["intercept_unary_unary"]) + assert ( + grpc_helpers_async.apply_channel_interceptors(channel, [interceptor]) is channel + ) + + +def test_apply_channel_interceptors_fallback_not_appendable(): + channel = mock.Mock(spec=["_unary_unary_interceptors"]) + channel._unary_unary_interceptors = 123 + interceptor = mock.Mock(spec=[]) + assert ( + grpc_helpers_async.apply_channel_interceptors(channel, [interceptor]) is channel + ) + + +def test_apply_channel_interceptors_fallback_unary_unary_list(): + class CustomInterceptor: + pass + + interceptor1 = CustomInterceptor() + interceptor2 = CustomInterceptor() + channel = mock.Mock(spec=["_unary_unary_interceptors"]) + channel._unary_unary_interceptors = [interceptor1] + + result = grpc_helpers_async.apply_channel_interceptors( + channel, [interceptor1, interceptor2] + ) + assert result is channel + assert channel._unary_unary_interceptors == [interceptor1, interceptor2] + + +def test_apply_channel_interceptors_fallback_unary_unary_appendable(): + class CustomInterceptor: + pass + + interceptor = CustomInterceptor() + channel = mock.Mock(spec=["_unary_unary_interceptors"]) + mock_append = mock.Mock(spec=["append"]) + channel._unary_unary_interceptors = mock_append + + result = grpc_helpers_async.apply_channel_interceptors(channel, [interceptor]) + assert result is channel + mock_append.append.assert_called_once_with(interceptor) diff --git a/packages/google-api-core/tests/unit/gapic/test_method.py b/packages/google-api-core/tests/unit/gapic/test_method.py index a8d2197b0d6a..18a20d8e1879 100644 --- a/packages/google-api-core/tests/unit/gapic/test_method.py +++ b/packages/google-api-core/tests/unit/gapic/test_method.py @@ -377,7 +377,7 @@ def test__deduplicate_metadata_tokens(headers, expected): ( { "method_name": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", - "kind": "rest", + "kind": "custom_unsupported", }, True, ), @@ -407,7 +407,7 @@ def test__deduplicate_metadata_tokens(headers, expected): "disabled_by_flag", "omitted_method_name", "streaming_skipped", - "rest_kind_skipped", + "custom_unsupported_kind_skipped", "rest_asyncio_kind_skipped", "grpc_asyncio_kind_skipped", "http_kind_skipped", @@ -439,7 +439,8 @@ def test_wrap_method_otel_tracing_skips_span(monkeypatch, kwargs, capabilities_e ) -def test_wrap_method_otel_tracing_enabled_success(mock_otel): +@pytest.mark.parametrize("kind", ["grpc", "rest"]) +def test_wrap_method_otel_tracing_enabled_success(mock_otel, kind): """Proves that when OpenTelemetry tracing is enabled and method_name is passed, a T3 client span is started.""" mock_target = mock.Mock(return_value="success") @@ -447,15 +448,19 @@ def test_wrap_method_otel_tracing_enabled_success(mock_otel): mock_target, default_timeout=60, method_name="/google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", - kind="grpc", + kind=kind, ) result = wrapped() assert result == "success" + expected_attributes = { + "rpc.system.name": "http" if kind == "rest" else "grpc", + "rpc.method": "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", + } mock_otel.tracer.start_as_current_span.assert_called_once_with( "google.cloud.secretmanager.v1.SecretManagerService/ListSecrets", kind="CLIENT", - attributes=_DEFAULT_SPAN_ATTRIBUTES, + attributes=expected_attributes, ) mock_otel.span.set_attribute.assert_called_with("rpc.response.status_code", "OK") @@ -525,9 +530,13 @@ def test_wrap_method_otel_tracing_enabled_error(mock_otel): wrapped() mock_target.assert_called_once() - mock_otel.span.set_attribute.assert_called_with( + mock_otel.span.set_attribute.assert_any_call( "rpc.response.status_code", "RuntimeError" ) + mock_otel.span.set_attribute.assert_any_call("error.type", "RuntimeError") + mock_otel.span.set_attribute.assert_any_call( + "status.message", "gRPC connection reset" + ) @pytest.mark.parametrize( @@ -547,7 +556,7 @@ def test_wrap_method_otel_tracing_enabled_error(mock_otel): def test_wrap_method_otel_tracing_error_status_code_mapping( mock_otel, exc, expected_status ): - """Proves that exceptions are cleanly mapped to canonical rpc.response.status_code names.""" + """Proves that exceptions are cleanly mapped to canonical rpc.response.status_code and error.type names.""" mock_target = mock.Mock(side_effect=exc) wrapped = google.api_core.gapic_v1.method.wrap_method( @@ -557,9 +566,12 @@ def test_wrap_method_otel_tracing_error_status_code_mapping( with pytest.raises(type(exc)): wrapped() - mock_otel.span.set_attribute.assert_called_with( + mock_otel.span.set_attribute.assert_any_call( "rpc.response.status_code", expected_status ) + mock_otel.span.set_attribute.assert_any_call("error.type", expected_status) + expected_msg = exc.cause.message if getattr(exc, "cause", None) else exc.message + mock_otel.span.set_attribute.assert_any_call("status.message", expected_msg) def test_wrap_method_otel_tracing_import_error(monkeypatch): @@ -687,11 +699,13 @@ def test_wrap_method_otel_tracing_attributes_no_service(mock_otel): def test_extract_error_attributes_standard_exception(): - """Proves that _extract_error_attributes returns empty dict for standard exceptions without ErrorInfo.""" - assert ( - google.api_core.gapic_v1.method._extract_error_attributes(ValueError("fail")) - == {} - ) + """Proves that _extract_error_attributes returns fallback error.type for exceptions without ErrorInfo.""" + assert google.api_core.gapic_v1.method._extract_error_attributes( + ValueError("fail") + ) == {"error.type": "ValueError", "status.message": "fail"} + assert google.api_core.gapic_v1.method._extract_error_attributes( + exceptions.InvalidArgument("invalid argument") + ) == {"error.type": "INVALID_ARGUMENT", "status.message": "invalid argument"} assert google.api_core.gapic_v1.method._extract_error_attributes(None) == {} @@ -745,6 +759,7 @@ def test_wrap_method_otel_tracing_records_gcp_error_attributes(mock_otel): mock_otel.span.set_attribute.assert_any_call( "gcp.errors.metadata.quota_limit", "100" ) + mock_otel.span.set_attribute.assert_any_call("status.message", "quota exceeded") def test_extract_status_code_variations(): @@ -838,12 +853,14 @@ def test_extract_error_attributes_variations(): "google.api_core.exceptions._parse_grpc_error_details", side_effect=ValueError("bad proto"), ): - assert _extract_error_attributes(exc_with_resp) == {} + assert _extract_error_attributes(exc_with_resp) == { + "error.type": "SimpleNamespace" + } # 4. error_info with empty domain, empty reason, empty metadata error_info_empty = types.SimpleNamespace(domain="", reason="", metadata=None) exc_empty = types.SimpleNamespace(error_info=error_info_empty) - assert _extract_error_attributes(exc_empty) == {} + assert _extract_error_attributes(exc_empty) == {"error.type": "SimpleNamespace"} # 5. else fallback where target_exc directly has domain, reason, and metadata exc_fallback = types.SimpleNamespace( @@ -863,7 +880,36 @@ def test_extract_error_attributes_variations(): reason="", metadata={}, ) - assert _extract_error_attributes(exc_fallback_empty) == {} + assert _extract_error_attributes(exc_fallback_empty) == { + "error.type": "SimpleNamespace" + } + + # 7. status.message extraction from .message attribute + exc_with_msg = types.SimpleNamespace(message="api call failed") + assert _extract_error_attributes(exc_with_msg) == { + "error.type": "SimpleNamespace", + "status.message": "api call failed", + } + + # 8. status.message extraction from .details() callable (e.g. gRPC RpcError) + exc_with_details = types.SimpleNamespace(details=lambda: "rpc deadline exceeded") + assert _extract_error_attributes(exc_with_details) == { + "error.type": "SimpleNamespace", + "status.message": "rpc deadline exceeded", + } + + # 9. status.message extraction from Exception string representation + exc_standard = ValueError("invalid argument passed") + assert _extract_error_attributes(exc_standard) == { + "error.type": "ValueError", + "status.message": "invalid argument passed", + } + + # 10. Exception with empty message string does not populate status.message + exc_empty_msg = ValueError("") + assert _extract_error_attributes(exc_empty_msg) == { + "error.type": "ValueError", + } def test_wrap_method_otel_tracing_partial_span_capabilities(mock_otel): @@ -881,9 +927,8 @@ def test_wrap_method_otel_tracing_partial_span_capabilities(mock_otel): ) with pytest.raises(ValueError): wrapped1() - mock_span1.set_attribute.assert_called_with( - "rpc.response.status_code", "ValueError" - ) + mock_span1.set_attribute.assert_any_call("rpc.response.status_code", "ValueError") + mock_span1.set_attribute.assert_any_call("error.type", "ValueError") # Test span without set_attribute (e.g. mock or stub lacking set_attribute) mock_span2 = mock.Mock(spec=[]) diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 4d7a0d283fd1..88066d69b3b6 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -13,6 +13,7 @@ # limitations under the License. import sys +import urllib.parse from unittest import mock import pytest @@ -94,6 +95,23 @@ def test_is_otel_capabilities_enabled_experimental_enabled_with_config(monkeypat assert _observability.is_otel_capabilities_enabled(options) +@pytest.mark.parametrize( + "boundary_options", + [ + pytest.param(None, id="options_none"), + pytest.param({}, id="options_empty_dict"), + pytest.param({"irrelevant_field": 123}, id="options_missing_tracer_provider"), + ], +) +def test_observability_handles_boundary_client_options(boundary_options): + """Verifies boundary handling when client options lack telemetry attributes.""" + enabled = _observability.is_otel_capabilities_enabled(boundary_options) + assert enabled is False + + endpoint_attrs = _observability._extract_endpoint_attributes(boundary_options) + assert endpoint_attrs == {"url.domain": "googleapis.com"} + + def test_get_tracer_provider_default(): """Proves that _get_tracer_provider returns None when no client_options are supplied.""" assert _observability._get_tracer_provider() is None @@ -542,3 +560,756 @@ def test_grpc_client_response_hook_error_status_value(): mock_span.status.status_code.value = 2 _observability._grpc_client_response_hook(mock_span, mock.Mock()) mock_span.set_attribute.assert_not_called() + + +def test_grpc_client_response_hook_none_span(): + """Proves that _grpc_client_response_hook gracefully handles span=None without error.""" + _observability._grpc_client_response_hook(None, mock.Mock()) + + +def test_get_otel_interceptor_sentinel_attribute(monkeypatch): + """Proves that get_otel_interceptor tags the returned closure with _is_otel_interceptor=True.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + options = ClientOptions() + + mock_otel = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock_otel.instrumentation.grpc, + ) + + interceptor = _observability.get_otel_interceptor(client_options=options) + assert callable(interceptor) + assert getattr(interceptor, "_is_otel_interceptor", None) is True + + +def test_build_http_span_attributes_with_request(): + """Proves that _build_http_span_attributes extracts attributes from a request object.""" + headers = {"key": "val"} + request = mock.Mock( + method="post", + url="https://example.com:8443/v1/echo", + headers=headers, + body=b"bytes-payload", + ) + name, attrs, res_headers = _observability._TraceContext._build_http_span_attributes( + request, url_template="/v1/echo" + ) + assert name == "POST" + assert attrs["http.request.method"] == "POST" + assert attrs["server.address"] == "example.com" + assert attrs["server.port"] == 8443 + assert attrs["url.template"] == "/v1/echo" + assert attrs["url.full"] == "https://example.com:8443/v1/echo" + assert attrs["http.request.body.size"] == len(b"bytes-payload") + assert res_headers is headers + + +def test_build_http_span_attributes_with_kwargs(): + """Proves that _build_http_span_attributes works with explicit kwargs and string body.""" + headers = {"key": "val"} + options = ClientOptions(api_endpoint="custom.endpoint.com:9443") + name, attrs, res_headers = _observability._TraceContext._build_http_span_attributes( + method="get", + url="https://custom.endpoint.com:9443/v1/items", + url_template="/v1/items", + headers=headers, + body="string-body", + client_options=options, + ) + assert name == "GET" + assert attrs["server.address"] == "custom.endpoint.com" + assert attrs["server.port"] == 9443 + assert attrs["http.request.body.size"] == len("string-body") + assert res_headers is headers + + +def test_build_http_span_attributes_client_options(): + """Proves that _build_http_span_attributes extracts server.address from client_options.""" + options = ClientOptions(api_endpoint="custom.endpoint.com:443") + name, attrs, res_headers = _observability._TraceContext._build_http_span_attributes( + method="GET", + url="https://custom.endpoint.com:443/test", + client_options=options, + ) + assert name == "GET" + assert attrs["server.address"] == "custom.endpoint.com" + + +def test_build_http_span_attributes_url_parsing_fallbacks(): + """Proves that _build_http_span_attributes gracefully handles empty or invalid URLs.""" + # Empty url + name, attrs, _ = _observability._TraceContext._build_http_span_attributes( + method="DELETE", url="" + ) + assert name == "DELETE" + assert attrs["server.address"] == "" + assert attrs["server.port"] == 443 + + # Malformed URL + with mock.patch.object( + urllib.parse, "urlsplit", side_effect=ValueError("boom"), autospec=True + ): + name, attrs, _ = _observability._TraceContext._build_http_span_attributes( + method="PUT", url="http://[invalid" + ) + assert name == "PUT" + assert attrs["server.address"] == "" + + +def test_trace_http_request_disabled(): + """Proves that trace_http_request yields a context with no active span when tracing is disabled.""" + request = mock.Mock(method="GET", url="https://example.com/api", headers={}) + with _observability.trace_http_request( + request, client_options=ClientOptions() + ) as ctx: + assert isinstance(ctx, _observability._TraceContext) + assert ctx._span is None + ctx.record_response(mock.Mock()) + + +def test_trace_http_request_active(monkeypatch): + """Proves that trace_http_request creates a span, sets attributes, and injects W3C headers.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_provider = mock.Mock() + mock_provider.get_tracer.return_value = mock_tracer + + mock_otel = mock.MagicMock() + mock_propagator = mock.Mock() + mock_otel.trace.propagation.tracecontext.TraceContextTextMapPropagator.return_value = mock_propagator + monkeypatch.setattr(_observability, "_TRACE_CONTEXT_PROPAGATOR", mock_propagator) + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions( + api_endpoint="custom.googleapis.com:8443", + tracer_provider=mock_provider, + ) + headers = {} + request = mock.Mock( + method="POST", + url="https://custom.googleapis.com:8443/v1/test", + headers=headers, + body=b"test-body", + ) + + with _observability.trace_http_request( + request, url_template="/v1/test", client_options=options + ) as ctx: + assert ctx._span is mock_span + + mock_tracer.start_as_current_span.assert_called_once() + call_args, call_kwargs = mock_tracer.start_as_current_span.call_args + assert call_args[0] == "POST" + attrs = call_kwargs["attributes"] + assert attrs["http.request.method"] == "POST" + assert attrs["server.address"] == "custom.googleapis.com" + assert attrs["server.port"] == 8443 + assert attrs["url.template"] == "/v1/test" + assert attrs["http.request.body.size"] == 9 + mock_propagator.inject.assert_called_once_with(headers) + + +def test_trace_context_record_response_success(monkeypatch): + """Proves that _TraceContext.record_response records status code and size attributes.""" + mock_span = mock.Mock() + response = mock.Mock(status_code=200, headers={"Content-Length": "42"}) + + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_response(response) + mock_span.set_attribute.assert_any_call("http.response.status_code", 200) + mock_span.set_attribute.assert_any_call("http.response.body.size", 42) + + +def test_trace_context_record_response_error_status(monkeypatch): + """Proves that _TraceContext.record_response sets error status on 4xx/5xx responses.""" + mock_span = mock.Mock() + response = mock.Mock(status_code=503, headers={}) + + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_response(response) + mock_span.set_attribute.assert_any_call("http.response.status_code", 503) + mock_span.set_status.assert_called_once() + + +def test_trace_context_record_error(monkeypatch): + """Proves that _TraceContext.record_error records exception and error attributes.""" + mock_span = mock.Mock() + exc = ValueError("Network failure") + + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_error(exc) + mock_span.record_exception.assert_called_once_with(exc) + mock_span.set_status.assert_called_once() + mock_span.set_attribute.assert_any_call("error.type", "ValueError") + mock_span.set_attribute.assert_any_call("status.message", "Network failure") + + # Also verify record_http_error alias + mock_span.reset_mock() + ctx.record_http_error(exc) + mock_span.record_exception.assert_called_once_with(exc) + + +def test_trace_http_request_with_kwargs(monkeypatch): + """Proves that trace_http_request works when invoked using keyword arguments only.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_provider = mock.Mock() + mock_provider.get_tracer.return_value = mock_tracer + + mock_otel = mock.MagicMock() + mock_propagator = mock.Mock() + mock_otel.trace.propagation.tracecontext.TraceContextTextMapPropagator.return_value = mock_propagator + monkeypatch.setattr(_observability, "_TRACE_CONTEXT_PROPAGATOR", mock_propagator) + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions( + api_endpoint="custom.googleapis.com:8443", + tracer_provider=mock_provider, + ) + headers = {} + + with _observability.trace_http_request( + method="post", + url="https://custom.googleapis.com:8443/v1/test", + headers=headers, + body="string-payload", + url_template="/v1/test", + client_options=options, + ) as ctx: + assert ctx._span is mock_span + + call_args, call_kwargs = mock_tracer.start_as_current_span.call_args + assert call_args[0] == "POST" + attrs = call_kwargs["attributes"] + assert attrs["http.request.method"] == "POST" + assert attrs["http.request.body.size"] == len("string-payload") + mock_propagator.inject.assert_called_once_with(headers) + + +def test_trace_http_request_client_options(monkeypatch): + """Proves that trace_http_request respects client_options when passed as keyword argument.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_provider = mock.Mock() + mock_provider.get_tracer.return_value = mock_tracer + + mock_otel = mock.MagicMock() + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.status", + mock.Mock(), + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions( + api_endpoint="custom.googleapis.com:8443", + tracer_provider=mock_provider, + ) + + ctx = _observability.trace_http_request( + method="GET", + url="https://custom.googleapis.com:8443/v1/test", + client_options=options, + ) + with ctx as trace_ctx: + assert trace_ctx._span is mock_span + # Test record_response method on trace context + mock_resp = mock.Mock(status_code=200, headers={"Content-Length": "42"}) + trace_ctx.record_response(mock_resp) + mock_span.set_attribute.assert_any_call("http.response.status_code", 200) + mock_span.set_attribute.assert_any_call("http.response.body.size", 42) + + +def test_trace_http_request_default_tracer_and_url_parse(monkeypatch): + """Proves that trace_http_request uses trace.get_tracer when tracer_provider is None, + and extracts server.address and port from url if not present in options. + """ + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_otel = mock.MagicMock() + mock_otel.trace.get_tracer.return_value = mock_tracer + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions() # No api_endpoint, tracer_provider=None + + with _observability.trace_http_request( + client_options=options, + method="GET", + url="https://parsed-host.org:9443/v1/items", + ) as ctx: + assert ctx._span is mock_span + + mock_otel.trace.get_tracer.assert_called_once_with("google.api_core") + call_args, call_kwargs = mock_tracer.start_as_current_span.call_args + attrs = call_kwargs["attributes"] + assert attrs["server.address"] == "parsed-host.org" + assert attrs["server.port"] == 9443 + + +def test_trace_http_request_propagator_error(monkeypatch): + """Proves that trace_http_request catches propagation errors silently.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_provider = mock.Mock() + mock_provider.get_tracer.return_value = mock_tracer + + mock_otel = mock.MagicMock() + mock_propagator = mock.Mock() + mock_propagator.inject.side_effect = RuntimeError("Propagator failed") + mock_otel.trace.propagation.tracecontext.TraceContextTextMapPropagator.return_value = mock_propagator + monkeypatch.setattr(_observability, "_TRACE_CONTEXT_PROPAGATOR", mock_propagator) + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions(tracer_provider=mock_provider) + headers = {} + + with _observability.trace_http_request( + client_options=options, + method="GET", + url="https://example.com", + headers=headers, + ) as ctx: + assert ctx._span is mock_span + + +def test_trace_http_request_unexpected_error(monkeypatch): + """Proves that trace_http_request yields None when an unexpected error occurs during setup.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_otel = mock.MagicMock() + mock_otel.trace.get_tracer.side_effect = RuntimeError("Unexpected tracer crash") + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions() + with _observability.trace_http_request( + client_options=options, + method="GET", + url="https://example.com", + ) as ctx: + assert isinstance(ctx, _observability._TraceContext) + assert ctx._span is None + ctx.record_response(mock.Mock()) + + +def test_trace_context_record_response_none_or_missing_attribute(): + """Proves that _TraceContext.record_response handles None or non-span gracefully.""" + ctx_none = _observability._TraceContext() + ctx_none._span = None + ctx_none.record_response(mock.Mock()) + + ctx_obj = _observability._TraceContext() + ctx_obj._span = object() + ctx_obj.record_response(mock.Mock()) + + +def test_trace_context_record_response_content_fallback_and_invalid_content_length( + monkeypatch, +): + """Proves that _TraceContext.record_response handles invalid Content-Length and falls back to _content.""" + mock_span = mock.Mock() + # Invalid Content-Length string + response_invalid_len = mock.Mock( + status_code=200, headers={"Content-Length": "not-an-int"} + ) + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_response(response_invalid_len) + mock_span.set_attribute.assert_called_once_with("http.response.status_code", 200) + + mock_span.reset_mock() + # No Content-Length header, but response._content is present + response_with_content = mock.Mock( + status_code=None, headers={}, _content=b"hello-content" + ) + ctx.record_response(response_with_content) + mock_span.set_attribute.assert_called_once_with( + "http.response.body.size", len(b"hello-content") + ) + + +def test_trace_context_record_response_exception_handled(monkeypatch): + """Proves that _TraceContext.record_response catches exceptions gracefully.""" + mock_span = mock.Mock() + mock_span.set_attribute.side_effect = RuntimeError("attribute error") + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + ctx = _observability._TraceContext() + ctx._span = mock_span + # Should not raise + ctx.record_response(mock.Mock(status_code=200, headers={})) + + +def test_trace_context_record_error_none_span(): + """Proves that _TraceContext.record_error handles span=None gracefully.""" + ctx = _observability._TraceContext() + ctx._span = None + ctx.record_error(ValueError("test")) + + +def test_trace_context_record_error_with_status_code_and_empty_msg(monkeypatch): + """Proves that _TraceContext.record_error uses exc.code or exc.status_code when present, + and skips status.message when str(exc) is empty. + """ + mock_span = mock.Mock() + exc = Exception() + exc.code = 404 + + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_error(exc) + mock_span.set_attribute.assert_any_call("error.type", "404") + # str(exc) is empty, status.message should not be set + calls = [c[0][0] for c in mock_span.set_attribute.call_args_list] + assert "status.message" not in calls + + +def test_trace_context_record_error_exception_handled(monkeypatch): + """Proves that _TraceContext.record_error catches exceptions gracefully.""" + mock_span = mock.Mock() + mock_span.record_exception.side_effect = RuntimeError("crash") + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + # Should not raise + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_error(ValueError("test")) + + +def test_trace_http_request_url_parse_exception(monkeypatch): + """Proves that trace_http_request handles url parsing errors gracefully.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_otel = mock.MagicMock() + mock_otel.trace.get_tracer.return_value = mock_tracer + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + with mock.patch.object( + urllib.parse, "urlsplit", side_effect=ValueError("Invalid URL"), autospec=True + ): + options = ClientOptions() + with _observability.trace_http_request( + client_options=options, + method="GET", + url="http://[invalid-url", + ) as ctx: + assert ctx._span is mock_span + + +def test_trace_http_request_empty_url(monkeypatch): + """Proves that trace_http_request works when url is empty or None.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_tracer = mock.MagicMock() + mock_span = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_otel = mock.MagicMock() + mock_otel.trace.get_tracer.return_value = mock_tracer + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions() + with _observability.trace_http_request( + client_options=options, + method="GET", + url="", + ) as ctx: + assert ctx._span is mock_span + + +def test_trace_context_record_response_content_len_error(monkeypatch): + """Proves _TraceContext.record_response catches errors in response._content length calculation.""" + mock_span = mock.Mock() + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + response = mock.Mock(status_code=200, headers={}) + # Set _content to an object that raises TypeError on len() + response._content = object() + + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_response(response) + + +def test_trace_context_record_error_partial_span(monkeypatch): + """Proves that _TraceContext.record_error handles spans with missing methods.""" + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + # Object lacking record_exception and set_status + class MinimalSpan: + def __init__(self): + self.attrs = {} + + def set_attribute(self, k, v): + self.attrs[k] = v + + span = MinimalSpan() + ctx = _observability._TraceContext() + ctx._span = span + ctx.record_error(ValueError("partial span")) + assert span.attrs["error.type"] == "ValueError" + + # Object lacking set_attribute + class NoAttrSpan: + def __init__(self): + self.recorded = False + self.status = None + + def record_exception(self, exc): + self.recorded = True + + def set_status(self, status): + self.status = status + + span2 = NoAttrSpan() + ctx2 = _observability._TraceContext() + ctx2._span = span2 + ctx2.record_error(ValueError("no attr span")) + assert span2.recorded is True + + +def test_trace_context_record_response_no_content_length_and_no_content(monkeypatch): + """Proves that _TraceContext.record_response handles responses with neither Content-Length nor _content.""" + mock_span = mock.Mock() + mock_status_mod = mock.Mock() + monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) + + response = mock.Mock(spec=["status_code", "headers"], status_code=200, headers={}) + ctx = _observability._TraceContext() + ctx._span = mock_span + ctx.record_response(response) + mock_span.set_attribute.assert_called_once_with("http.response.status_code", 200) + + +def test_trace_http_request_records_error_and_reraises(monkeypatch): + """Proves that trace_http_request records error on active span when exception occurs.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_span = mock.MagicMock() + mock_tracer = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_otel = mock.MagicMock() + mock_otel.trace.get_tracer.return_value = mock_tracer + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.status", + mock_otel.trace.status, + ) + + err = RuntimeError("network broke") + with pytest.raises(RuntimeError, match="network broke"): + with _observability.trace_http_request( + method="GET", + url="https://example.com/fail", + headers={}, + ): + raise err + + mock_span.record_exception.assert_called_once_with(err) + mock_span.set_status.assert_called_once() + mock_span.set_attribute.assert_any_call("error.type", "RuntimeError") + mock_span.set_attribute.assert_any_call("status.message", "network broke") + + +def test_trace_http_request_no_multi_yield_bug(monkeypatch): + """Proves that exceptions in caller block cleanly propagate without RuntimeError.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + mock_span = mock.MagicMock() + mock_tracer = mock.MagicMock() + mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span + + mock_otel = mock.MagicMock() + mock_otel.trace.get_tracer.return_value = mock_tracer + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem(sys.modules, "opentelemetry.trace", mock_otel.trace) + monkeypatch.setitem( + sys.modules, + "opentelemetry.trace.propagation.tracecontext", + mock_otel.trace.propagation.tracecontext, + ) + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + + err = ConnectionResetError("connection reset by peer") + with pytest.raises(ConnectionResetError, match="connection reset by peer"): + with _observability.trace_http_request( + method="GET", + url="https://example.com/api", + ): + raise err + + +def test_trace_http_request_initialization_fails_open(monkeypatch): + """Proves that unexpected exceptions during telemetry initialization fail open and yield None.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + monkeypatch.setitem( + sys.modules, + "opentelemetry.instrumentation.grpc", + mock.Mock(), + ) + with mock.patch( + "opentelemetry.trace.get_tracer", + side_effect=RuntimeError("OTel crashed"), + ): + with _observability.trace_http_request( + client_options=ClientOptions(), + method="GET", + url="https://example.com", + ) as ctx: + assert isinstance(ctx, _observability._TraceContext) + assert ctx._span is None + ctx.record_response(mock.Mock()) From fbdbd739e9e0440dfd2fcb6f1971327b22db177e Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 6 Oct 2026 13:31:54 -0400 Subject: [PATCH 2/3] fix(observability): address review feedback on channel interceptors, error recording, and templates - Hoist sync channel interceptors (apply_channel_interceptors) and async channel interceptors (apply_async_channel_interceptors) to _compat with active fallback wrapping, eliminating the noop lambda fallback in grpc.py.j2 - Hoist wrap_method tracing and kind support constants to _compat and eliminate inspect.signature module-load checks in base transports - Adopt hybrid error recording in _TraceContext.record_error to enrich span attributes (error.type, status.message) while allowing upstream OpenTelemetry defaults to handle exception events - Pass parameterized proto route template from http_options into trace_http_request url_template - Add rest_transport.kind assertion for AsyncResumableUploadServiceRestTransport to maintain 100% test coverage - Add unit test isolation for custom tracer provider and test cases for _compat interceptors --- .../%name_%version/%sub/_compat.py.j2 | 44 ++++++++++++++++--- .../%sub/services/%service/_shared_macros.j2 | 6 ++- .../services/%service/transports/base.py.j2 | 19 +++----- .../services/%service/transports/grpc.py.j2 | 13 ++---- .../%service/transports/grpc_asyncio.py.j2 | 4 +- .../%name_%version/%sub/test_%service.py.j2 | 1 + .../%name_%version/%sub/test_compat.py.j2 | 8 ++++ .../system/telemetry_requirements_matrix.csv | 6 +-- .../tests/system/test_tracing.py | 10 ++--- .../google/api_core/_observability.py | 22 +++++----- .../tests/unit/test_observability.py | 25 +++-------- 11 files changed, 90 insertions(+), 68 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 index 59c040fe4eab..3c14eb41c04b 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/_compat.py.j2 @@ -32,6 +32,11 @@ from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + {% if has_auto_populated_fields %} if TYPE_CHECKING: # pragma: NO COVER import proto # type: ignore[import-untyped] @@ -90,12 +95,33 @@ ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" ) +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + try: from google.api_core import grpc_helpers_async except ImportError: # pragma: NO COVER grpc_helpers_async = None # type: ignore[assignment] +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + def _fallback_apply_interceptors( channel: Any, interceptors: Optional[Sequence[Any]] = None ) -> Any: # pragma: NO COVER @@ -153,12 +179,18 @@ def _fallback_apply_interceptors( return channel -if grpc_helpers_async is not None and hasattr( - grpc_helpers_async, "apply_channel_interceptors" -): - apply_channel_interceptors = grpc_helpers_async.apply_channel_interceptors # pragma: NO COVER -else: # pragma: NO COVER - apply_channel_interceptors = _fallback_apply_interceptors +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors try: diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index dca5ad63bf30..46d9eef0c88a 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -160,7 +160,8 @@ def _get_http_options(): timeout, transcoded_request, body=None, - client_options=None): + client_options=None, + url_template=None): """Execute the HTTP request over the transport session with OpenTelemetry tracing and metadata propagation.""" @@ -174,7 +175,7 @@ def _get_http_options(): client_options=client_options, method=method, url=url, - url_template=uri, + url_template=url_template, headers=headers, body=body, ) as trace_ctx: @@ -265,6 +266,7 @@ def _get_http_options(): body, {% endif %} client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index a8a38d09cce7..fe12fbdcc411 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,11 +3,16 @@ {% block content %} import abc -import inspect from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}} import gapic_version as package_version +from {{package_path}}._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core @@ -58,16 +63,6 @@ from {{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + ser DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(gapic_version=package_version.__version__) DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ -# Check once at module load time whether google-api-core's wrap_methods support -# OpenTelemetry tracing arguments (client_options, method_name, is_streaming, kind) -# to avoid recurring inspect.signature latency during client instantiation. -_WRAP_METHOD_SUPPORTS_TRACING = ( - "client_options" in inspect.signature(gapic_v1.method.wrap_method).parameters -) -_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = ( - "client_options" in inspect.signature(gapic_v1.method_async.wrap_method).parameters -) - class {{ service.name }}Transport(abc.ABC): """Abstract transport class for {{ service.name }}.""" @@ -184,7 +179,7 @@ class {{ service.name }}Transport(abc.ABC): stripped for backward compatibility with older `google-api-core` versions. """ - if _WRAP_METHOD_SUPPORTS_TRACING: + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER kwargs["client_options"] = self._client_options try: kind = self.kind diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 67137ffa5354..f54cf0d0c739 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -12,7 +12,7 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from typing import TYPE_CHECKING {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} -from {{package_path}}._compat import _observability +from {{package_path}}._compat import _observability, apply_channel_interceptors from google.api_core import client_options as client_options_lib {% if service.has_resumable_upload_methods %} @@ -193,7 +193,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): If not set, the host value will be used as a default. interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): Additional interceptors (or callables that apply interceptors) to apply to the - gRPC channel. Note: applying interceptors requires google-api-core >= 2.36.0. + gRPC channel. client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): Custom options for the client, containing options such as custom OpenTelemetry tracer providers. @@ -295,15 +295,10 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): ): channel_interceptors.append(otel_interceptor) - apply_interceptors = getattr( - grpc_helpers, - "apply_channel_interceptors", - lambda channel, interceptors: channel, - ) - self._grpc_channel = apply_interceptors(self._grpc_channel, channel_interceptors) + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index e28a1c529570..ba5aebc31a4d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -13,7 +13,7 @@ from typing import {% if service.has_resumable_upload_methods %}Any, {% endif %} from typing import TYPE_CHECKING {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} -from {{package_path}}._compat import _observability, apply_channel_interceptors +from {{package_path}}._compat import _observability, apply_async_channel_interceptors from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 @@ -347,7 +347,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): 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_channel_interceptors(self._grpc_channel, channel_interceptors) + self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index d1ff027ddf73..53accce6375d 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1837,6 +1837,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a result1 = await stub({{ method.input.ident }}()) rest_transport = transport._rest_transport assert rest_transport is not None + assert rest_transport.kind == "rest_asyncio" assert isinstance(result1, {{ method.client_output_async.ident }}) result2 = await stub({{ method.input.ident }}()) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 index 07a154b8dcf0..dca023a9bcd7 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_compat.py.j2 @@ -32,6 +32,7 @@ from {{package_path}}._compat import ( _observability, trace_http_request, apply_channel_interceptors, + apply_async_channel_interceptors, ) {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id @@ -602,4 +603,11 @@ def test_apply_channel_interceptors_compat(): result = apply_channel_interceptors(dummy_channel, None) assert result is dummy_channel + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + {% endblock %} diff --git a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv index 9e6841643553..b794cab4d1d6 100644 --- a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv +++ b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv @@ -3,7 +3,7 @@ F1.1,Tracing Off (HTTP),T4,HTTP/REST,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N F1.2,Unary Success (HTTP),T4,HTTP/REST,Happy Path,1,CLIENT,POST,OK,NOT SET,NOT SET,NOT SET,POST,200,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"url.full, http.request.body.size" F1.3,Server Error (HTTP),T4,HTTP/REST,Server Failure,1,CLIENT,POST,ERROR,NOT SET,NOT SET,NOT SET,POST,400,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"url.full, http.request.body.size" F1.4,Client Timeout (HTTP),T4,HTTP/REST,Client Timeout,1,CLIENT,POST,ERROR,NOT SET,NOT SET,NOT SET,POST,504,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full -F1.5,Retry Recovery (HTTP),T4,HTTP/REST,Retry Recovery,2,CLIENT,POST,ERROR | OK,NOT SET,NOT SET,NOT SET,POST,503 | 200,NOT SET,googleapis.com,/v1beta1/sequences/*,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full +F1.5,Retry Recovery (HTTP),T4,HTTP/REST,Retry Recovery,2,CLIENT,POST,ERROR | OK,NOT SET,NOT SET,NOT SET,POST,503 | 200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full F1.6,Tracing Off (gRPC),T4,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted F1.7,Unary Success (gRPC),T4,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" F1.8,Server Error (gRPC),T4,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,3,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" @@ -17,7 +17,7 @@ F2.5,Tracing Off (gRPC),T3,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/ F2.6,Unary Success (gRPC),T3,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) F2.7,Server Error (gRPC),T3,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,INVALID_ARGUMENT,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,INVALID_ARGUMENT,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" F2.8,Client Timeout (gRPC),T3,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,DEADLINE_EXCEEDED,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,DEADLINE_EXCEEDED,Client deadline exceeded,NOT SET,None (Root),gcp.errors.domain -F3.1,Retry Succeeds (HTTP),T3 + T4,HTTP/REST,Retry Recovery,3,CLIENT,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: POST,T3: UNSET | T4: ERROR | OK,T3: http | T4: NOT SET,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: NOT SET,T3: OK | T4: NOT SET,T3: NOT SET | T4: POST,T3: NOT SET | T4: 503 | 200,NOT SET,T3: NOT SET | T4: googleapis.com,T3: NOT SET | T4: /v1beta1/sequences/*,T3: NOT SET | T4: localhost,T3: NOT SET | T4: 7469,NOT SET,NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.2,Retries Exhausted (HTTP),T3 + T4,HTTP/REST,Retries Exhausted,N + 1,CLIENT,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: POST,T3: ERROR | T4: ERROR,T3: http | T4: NOT SET,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: NOT SET,T3: UNAVAILABLE | T4: NOT SET,T3: NOT SET | T4: POST,T3: NOT SET | T4: 503,NOT SET,T3: NOT SET | T4: googleapis.com,T3: NOT SET | T4: /v1beta1/sequences/*,T3: NOT SET | T4: localhost,T3: NOT SET | T4: 7469,T3: UNAVAILABLE | T4: NOT SET,T3: Persistent outage | T4: NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.1,Retry Succeeds (HTTP),T3 + T4,HTTP/REST,Retry Recovery,3,CLIENT,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: POST,T3: UNSET | T4: ERROR | OK,T3: http | T4: NOT SET,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: NOT SET,T3: OK | T4: NOT SET,T3: NOT SET | T4: POST,T3: NOT SET | T4: 503 | 200,NOT SET,T3: NOT SET | T4: googleapis.com,T3: NOT SET | T4: /v1beta1/{name=sequences/*},T3: NOT SET | T4: localhost,T3: NOT SET | T4: 7469,NOT SET,NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.2,Retries Exhausted (HTTP),T3 + T4,HTTP/REST,Retries Exhausted,N + 1,CLIENT,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: POST,T3: ERROR | T4: ERROR,T3: http | T4: NOT SET,T3: google.showcase.v1beta1.SequenceService/AttemptSequence | T4: NOT SET,T3: UNAVAILABLE | T4: NOT SET,T3: NOT SET | T4: POST,T3: NOT SET | T4: 503,NOT SET,T3: NOT SET | T4: googleapis.com,T3: NOT SET | T4: /v1beta1/{name=sequences/*},T3: NOT SET | T4: localhost,T3: NOT SET | T4: 7469,T3: UNAVAILABLE | T4: NOT SET,T3: Persistent outage | T4: NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 F3.3,Retry Succeeds (gRPC),T3 + T4,gRPC,Retry Recovery,3,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: UNSET | T4: ERROR | UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: OK | T4: NOT SET | OK,NOT SET,NOT SET,T3: NOT SET | T4: 14 | 0,T3: NOT SET | T4: googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates OK; T4 children nested under T3 F3.4,Retries Exhausted (gRPC),T3 + T4,gRPC,Retries Exhausted,N + 1,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: ERROR | T4: ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,T3: UNAVAILABLE | T4: NOT SET,NOT SET,NOT SET,T3: NOT SET | T4: 14,T3: NOT SET | T4: googleapis.com,NOT SET,NOT SET,NOT SET,T3: UNAVAILABLE | T4: NOT SET,T3: Persistent outage | T4: NOT SET,NOT SET,T3: None (Root) | T4: T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 diff --git a/packages/gapic-generator/tests/system/test_tracing.py b/packages/gapic-generator/tests/system/test_tracing.py index 132fbac9aca7..7f2ca695a60b 100644 --- a/packages/gapic-generator/tests/system/test_tracing.py +++ b/packages/gapic-generator/tests/system/test_tracing.py @@ -155,10 +155,10 @@ def test_custom_tracer_provider(use_mtls): global_provider = TracerProvider() global_provider.add_span_processor(SimpleSpanProcessor(global_exporter)) - # Temporarily set the ambient global tracer provider - original_provider = trace.get_tracer_provider() - trace.set_tracer_provider(global_provider) - try: + # Mock the ambient global tracer provider instead of mutating global state + with mock.patch( + "opentelemetry.trace.get_tracer_provider", return_value=global_provider + ): options = ClientOptions( tracer_provider=custom_provider, ) @@ -179,8 +179,6 @@ def test_custom_tracer_provider(use_mtls): assert len(custom_spans) == 2 global_spans = global_exporter.get_finished_spans() assert len(global_spans) == 0 - finally: - trace.set_tracer_provider(original_provider) def test_direct_client_initialization_tracing(span_exporter): diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index a7a1dd5d2a89..cddc579b99bf 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -451,7 +451,18 @@ def record_response(self, response: Any) -> None: record_http_response = record_response def record_error(self, exc: BaseException | None) -> None: - """Record an HTTP error/exception on the active span. + """Record supplemental Cloud Observability error attributes on the active span. + + Upstream OpenTelemetry's ``start_as_current_span`` context manager automatically + records the exception event and marks the span status as ERROR when an unhandled + exception escapes (via its default ``record_exception=True`` and + ``set_status_on_exception=True`` settings). + + This method supplements that base OpenTelemetry behavior by enriching the span + with semantic attributes required by Cloud Trace and the Google Cloud Observability + specification: + - ``error.type``: Low-cardinality status code string (e.g. "404") or exception class name. + - ``status.message``: Error description message. Args: exc (Optional[BaseException]): The exception raised during dispatch. @@ -461,15 +472,6 @@ def record_error(self, exc: BaseException | None) -> None: return try: - from opentelemetry.trace.status import ( # type: ignore[import-not-found] - Status, - StatusCode, - ) - - if hasattr(span, "record_exception"): - span.record_exception(exc) - if hasattr(span, "set_status"): - span.set_status(Status(StatusCode.ERROR)) if hasattr(span, "set_attribute"): status_code = getattr(exc, "code", None) or getattr( exc, "status_code", None diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 88066d69b3b6..90ac4acb3ba3 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -762,7 +762,7 @@ def test_trace_context_record_response_error_status(monkeypatch): def test_trace_context_record_error(monkeypatch): - """Proves that _TraceContext.record_error records exception and error attributes.""" + """Proves that _TraceContext.record_error records supplemental error attributes.""" mock_span = mock.Mock() exc = ValueError("Network failure") @@ -772,15 +772,14 @@ def test_trace_context_record_error(monkeypatch): ctx = _observability._TraceContext() ctx._span = mock_span ctx.record_error(exc) - mock_span.record_exception.assert_called_once_with(exc) - mock_span.set_status.assert_called_once() mock_span.set_attribute.assert_any_call("error.type", "ValueError") mock_span.set_attribute.assert_any_call("status.message", "Network failure") # Also verify record_http_error alias mock_span.reset_mock() ctx.record_http_error(exc) - mock_span.record_exception.assert_called_once_with(exc) + mock_span.set_attribute.assert_any_call("error.type", "ValueError") + mock_span.set_attribute.assert_any_call("status.message", "Network failure") def test_trace_http_request_with_kwargs(monkeypatch): @@ -1075,7 +1074,7 @@ def test_trace_context_record_error_with_status_code_and_empty_msg(monkeypatch): def test_trace_context_record_error_exception_handled(monkeypatch): """Proves that _TraceContext.record_error catches exceptions gracefully.""" mock_span = mock.Mock() - mock_span.record_exception.side_effect = RuntimeError("crash") + mock_span.set_attribute.side_effect = RuntimeError("crash") mock_status_mod = mock.Mock() monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) @@ -1172,7 +1171,7 @@ def test_trace_context_record_error_partial_span(monkeypatch): mock_status_mod = mock.Mock() monkeypatch.setitem(sys.modules, "opentelemetry.trace.status", mock_status_mod) - # Object lacking record_exception and set_status + # Object with set_attribute class MinimalSpan: def __init__(self): self.attrs = {} @@ -1188,21 +1187,13 @@ def set_attribute(self, k, v): # Object lacking set_attribute class NoAttrSpan: - def __init__(self): - self.recorded = False - self.status = None - - def record_exception(self, exc): - self.recorded = True - - def set_status(self, status): - self.status = status + pass span2 = NoAttrSpan() ctx2 = _observability._TraceContext() ctx2._span = span2 + # Should not raise AttributeError ctx2.record_error(ValueError("no attr span")) - assert span2.recorded is True def test_trace_context_record_response_no_content_length_and_no_content(monkeypatch): @@ -1255,8 +1246,6 @@ def test_trace_http_request_records_error_and_reraises(monkeypatch): ): raise err - mock_span.record_exception.assert_called_once_with(err) - mock_span.set_status.assert_called_once() mock_span.set_attribute.assert_any_call("error.type", "RuntimeError") mock_span.set_attribute.assert_any_call("status.message", "network broke") From 1ffdedb199e98203a8b73dd7cefb4ba2b8991004 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 6 Oct 2026 13:32:14 -0400 Subject: [PATCH 3/3] test(gapic): update bazel integration goldens for resumable upload and otel tracing --- .../asset/google/cloud/asset_v1/_compat.py | 160 +- .../asset_v1/services/asset_service/client.py | 37 +- .../services/asset_service/transports/base.py | 136 +- .../services/asset_service/transports/grpc.py | 30 +- .../asset_service/transports/grpc_asyncio.py | 114 +- .../services/asset_service/transports/rest.py | 1063 +++++++-- .../asset_service/transports/rest_base.py | 10 +- .../unit/gapic/asset_v1/test_asset_service.py | 187 ++ .../tests/unit/gapic/asset_v1/test_compat.py | 81 +- .../google/iam/credentials_v1/_compat.py | 160 +- .../services/iam_credentials/client.py | 37 +- .../iam_credentials/transports/base.py | 76 +- .../iam_credentials/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 54 +- .../iam_credentials/transports/rest.py | 197 +- .../iam_credentials/transports/rest_base.py | 10 +- .../unit/gapic/credentials_v1/test_compat.py | 81 +- .../credentials_v1/test_iam_credentials.py | 187 ++ .../google/cloud/eventarc_v1/_compat.py | 160 +- .../eventarc_v1/services/eventarc/client.py | 37 +- .../services/eventarc/transports/base.py | 208 +- .../services/eventarc/transports/grpc.py | 30 +- .../eventarc/transports/grpc_asyncio.py | 186 +- .../services/eventarc/transports/rest.py | 2119 +++++++++++++---- .../services/eventarc/transports/rest_base.py | 10 +- .../unit/gapic/eventarc_v1/test_compat.py | 81 +- .../unit/gapic/eventarc_v1/test_eventarc.py | 187 ++ .../google/cloud/logging_v2/_compat.py | 160 +- .../services/config_service_v2/client.py | 37 +- .../config_service_v2/transports/base.py | 169 +- .../config_service_v2/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 147 +- .../services/logging_service_v2/client.py | 37 +- .../logging_service_v2/transports/base.py | 92 +- .../logging_service_v2/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 70 +- .../services/metrics_service_v2/client.py | 37 +- .../metrics_service_v2/transports/base.py | 88 +- .../metrics_service_v2/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 66 +- .../unit/gapic/logging_v2/test_compat.py | 81 +- .../logging_v2/test_config_service_v2.py | 187 ++ .../logging_v2/test_logging_service_v2.py | 187 ++ .../logging_v2/test_metrics_service_v2.py | 187 ++ .../google/cloud/logging_v2/_compat.py | 160 +- .../services/config_service_v2/client.py | 37 +- .../config_service_v2/transports/base.py | 169 +- .../config_service_v2/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 147 +- .../services/logging_service_v2/client.py | 37 +- .../logging_service_v2/transports/base.py | 92 +- .../logging_service_v2/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 70 +- .../services/metrics_service_v2/client.py | 37 +- .../metrics_service_v2/transports/base.py | 88 +- .../metrics_service_v2/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 66 +- .../unit/gapic/logging_v2/test_compat.py | 81 +- .../logging_v2/test_config_service_v2.py | 187 ++ .../logging_v2/test_logging_service_v2.py | 187 ++ .../logging_v2/test_metrics_service_v2.py | 187 ++ .../redis/google/cloud/redis_v1/_compat.py | 160 +- .../redis_v1/services/cloud_redis/client.py | 41 +- .../services/cloud_redis/transports/base.py | 118 +- .../services/cloud_redis/transports/grpc.py | 30 +- .../cloud_redis/transports/grpc_asyncio.py | 96 +- .../services/cloud_redis/transports/rest.py | 804 +++++-- .../cloud_redis/transports/rest_asyncio.py | 858 +++++-- .../cloud_redis/transports/rest_base.py | 10 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 187 ++ .../tests/unit/gapic/redis_v1/test_compat.py | 81 +- .../google/cloud/redis_v1/_compat.py | 160 +- .../redis_v1/services/cloud_redis/client.py | 41 +- .../services/cloud_redis/transports/base.py | 100 +- .../services/cloud_redis/transports/grpc.py | 30 +- .../cloud_redis/transports/grpc_asyncio.py | 78 +- .../services/cloud_redis/transports/rest.py | 531 ++++- .../cloud_redis/transports/rest_asyncio.py | 573 ++++- .../cloud_redis/transports/rest_base.py | 10 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 187 ++ .../tests/unit/gapic/redis_v1/test_compat.py | 81 +- .../google/showcase_v1beta1/_compat.py | 165 +- .../showcase_v1beta1/services/echo/client.py | 37 +- .../services/echo/transports/base.py | 127 +- .../services/echo/transports/grpc.py | 30 +- .../services/echo/transports/grpc_asyncio.py | 105 +- .../services/echo/transports/rest.py | 817 +++++-- .../services/echo/transports/rest_base.py | 10 +- .../services/identity/client.py | 37 +- .../services/identity/transports/base.py | 106 +- .../services/identity/transports/grpc.py | 30 +- .../identity/transports/grpc_asyncio.py | 84 +- .../services/identity/transports/rest.py | 617 +++-- .../services/identity/transports/rest_base.py | 10 +- .../services/messaging/client.py | 37 +- .../services/messaging/transports/base.py | 136 +- .../services/messaging/transports/grpc.py | 30 +- .../messaging/transports/grpc_asyncio.py | 114 +- .../services/messaging/transports/rest.py | 935 ++++++-- .../messaging/transports/rest_base.py | 10 +- .../resumable_upload_service/client.py | 37 +- .../transports/base.py | 94 +- .../transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 72 +- .../transports/rest.py | 399 +++- .../transports/rest_asyncio.py | 29 +- .../transports/rest_base.py | 10 +- .../services/sequence_service/client.py | 37 +- .../sequence_service/transports/base.py | 110 +- .../sequence_service/transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 88 +- .../sequence_service/transports/rest.py | 668 ++++-- .../sequence_service/transports/rest_base.py | 10 +- .../services/testing/client.py | 37 +- .../services/testing/transports/base.py | 115 +- .../services/testing/transports/grpc.py | 30 +- .../testing/transports/grpc_asyncio.py | 93 +- .../services/testing/transports/rest.py | 742 ++++-- .../services/testing/transports/rest_base.py | 10 +- .../gapic/showcase_v1beta1/test_compat.py | 81 +- .../unit/gapic/showcase_v1beta1/test_echo.py | 187 ++ .../gapic/showcase_v1beta1/test_identity.py | 187 ++ .../gapic/showcase_v1beta1/test_messaging.py | 187 ++ .../test_resumable_upload_service.py | 188 ++ .../showcase_v1beta1/test_sequence_service.py | 187 ++ .../gapic/showcase_v1beta1/test_testing.py | 187 ++ .../storagebatchoperations_v1/_compat.py | 165 +- .../storage_batch_operations/client.py | 37 +- .../transports/base.py | 103 +- .../transports/grpc.py | 30 +- .../transports/grpc_asyncio.py | 81 +- .../transports/rest.py | 574 ++++- .../transports/rest_base.py | 10 +- .../storagebatchoperations_v1/test_compat.py | 81 +- .../test_storage_batch_operations.py | 187 ++ 135 files changed, 18103 insertions(+), 3561 deletions(-) diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py index ffc75791c484..cb64bd630fb1 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.asset_v1._compat import _observability from google.cloud.asset_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -545,18 +546,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., AssetServiceTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py index 2afbe7e1d6c8..8840dbfc0fd7 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.asset_v1 import gapic_version as package_version +from google.cloud.asset_v1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -55,6 +62,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -85,6 +93,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -122,26 +133,79 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.export_assets: gapic_v1.method.wrap_method( + self.export_assets: self._wrap_method( self.export_assets, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ExportAssets", ), - self.list_assets: gapic_v1.method.wrap_method( + self.list_assets: self._wrap_method( self.list_assets, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ListAssets", ), - self.batch_get_assets_history: gapic_v1.method.wrap_method( + self.batch_get_assets_history: self._wrap_method( self.batch_get_assets_history, default_retry=retries.Retry( initial=0.1, @@ -155,13 +219,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/BatchGetAssetsHistory", ), - self.create_feed: gapic_v1.method.wrap_method( + self.create_feed: self._wrap_method( self.create_feed, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/CreateFeed", ), - self.get_feed: gapic_v1.method.wrap_method( + self.get_feed: self._wrap_method( self.get_feed, default_retry=retries.Retry( initial=0.1, @@ -175,8 +241,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/GetFeed", ), - self.list_feeds: gapic_v1.method.wrap_method( + self.list_feeds: self._wrap_method( self.list_feeds, default_retry=retries.Retry( initial=0.1, @@ -190,13 +257,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ListFeeds", ), - self.update_feed: gapic_v1.method.wrap_method( + self.update_feed: self._wrap_method( self.update_feed, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/UpdateFeed", ), - self.delete_feed: gapic_v1.method.wrap_method( + self.delete_feed: self._wrap_method( self.delete_feed, default_retry=retries.Retry( initial=0.1, @@ -210,8 +279,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/DeleteFeed", ), - self.search_all_resources: gapic_v1.method.wrap_method( + self.search_all_resources: self._wrap_method( self.search_all_resources, default_retry=retries.Retry( initial=0.1, @@ -225,8 +295,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=15.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/SearchAllResources", ), - self.search_all_iam_policies: gapic_v1.method.wrap_method( + self.search_all_iam_policies: self._wrap_method( self.search_all_iam_policies, default_retry=retries.Retry( initial=0.1, @@ -240,8 +311,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=15.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/SearchAllIamPolicies", ), - self.analyze_iam_policy: gapic_v1.method.wrap_method( + self.analyze_iam_policy: self._wrap_method( self.analyze_iam_policy, default_retry=retries.Retry( initial=0.1, @@ -254,71 +326,85 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=300.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeIamPolicy", ), - self.analyze_iam_policy_longrunning: gapic_v1.method.wrap_method( + self.analyze_iam_policy_longrunning: self._wrap_method( self.analyze_iam_policy_longrunning, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeIamPolicyLongrunning", ), - self.analyze_move: gapic_v1.method.wrap_method( + self.analyze_move: self._wrap_method( self.analyze_move, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeMove", ), - self.query_assets: gapic_v1.method.wrap_method( + self.query_assets: self._wrap_method( self.query_assets, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/QueryAssets", ), - self.create_saved_query: gapic_v1.method.wrap_method( + self.create_saved_query: self._wrap_method( self.create_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/CreateSavedQuery", ), - self.get_saved_query: gapic_v1.method.wrap_method( + self.get_saved_query: self._wrap_method( self.get_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/GetSavedQuery", ), - self.list_saved_queries: gapic_v1.method.wrap_method( + self.list_saved_queries: self._wrap_method( self.list_saved_queries, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ListSavedQueries", ), - self.update_saved_query: gapic_v1.method.wrap_method( + self.update_saved_query: self._wrap_method( self.update_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/UpdateSavedQuery", ), - self.delete_saved_query: gapic_v1.method.wrap_method( + self.delete_saved_query: self._wrap_method( self.delete_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/DeleteSavedQuery", ), - self.batch_get_effective_iam_policies: gapic_v1.method.wrap_method( + self.batch_get_effective_iam_policies: self._wrap_method( self.batch_get_effective_iam_policies, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/BatchGetEffectiveIamPolicies", ), - self.analyze_org_policies: gapic_v1.method.wrap_method( + self.analyze_org_policies: self._wrap_method( self.analyze_org_policies, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeOrgPolicies", ), - self.analyze_org_policy_governed_containers: gapic_v1.method.wrap_method( + self.analyze_org_policy_governed_containers: self._wrap_method( self.analyze_org_policy_governed_containers, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeOrgPolicyGovernedContainers", ), - self.analyze_org_policy_governed_assets: gapic_v1.method.wrap_method( + self.analyze_org_policy_governed_assets: self._wrap_method( self.analyze_org_policy_governed_assets, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeOrgPolicyGovernedAssets", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py index 848bb1096cbe..53964d2306d1 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.asset_v1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -36,6 +40,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import AssetServiceTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -132,6 +140,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -182,6 +192,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -238,6 +254,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -259,8 +276,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py index 8fb1179f2fde..7a8f6ff641cc 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.asset_v1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -40,6 +43,7 @@ from .base import AssetServiceTransport, DEFAULT_CLIENT_INFO from .grpc import AssetServiceGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -179,6 +183,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -230,6 +236,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -285,6 +297,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -306,10 +319,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -1054,19 +1080,24 @@ def analyze_org_policy_governed_assets(self) -> Callable[ return self._stubs['analyze_org_policy_governed_assets'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.export_assets: self._wrap_method( + self.export_assets: self._wrap_async_method( self.export_assets, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ExportAssets", ), - self.list_assets: self._wrap_method( + self.list_assets: self._wrap_async_method( self.list_assets, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ListAssets", ), - self.batch_get_assets_history: self._wrap_method( + self.batch_get_assets_history: self._wrap_async_method( self.batch_get_assets_history, default_retry=retries.AsyncRetry( initial=0.1, @@ -1080,13 +1111,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/BatchGetAssetsHistory", ), - self.create_feed: self._wrap_method( + self.create_feed: self._wrap_async_method( self.create_feed, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/CreateFeed", ), - self.get_feed: self._wrap_method( + self.get_feed: self._wrap_async_method( self.get_feed, default_retry=retries.AsyncRetry( initial=0.1, @@ -1100,8 +1133,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/GetFeed", ), - self.list_feeds: self._wrap_method( + self.list_feeds: self._wrap_async_method( self.list_feeds, default_retry=retries.AsyncRetry( initial=0.1, @@ -1115,13 +1149,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ListFeeds", ), - self.update_feed: self._wrap_method( + self.update_feed: self._wrap_async_method( self.update_feed, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/UpdateFeed", ), - self.delete_feed: self._wrap_method( + self.delete_feed: self._wrap_async_method( self.delete_feed, default_retry=retries.AsyncRetry( initial=0.1, @@ -1135,8 +1171,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/DeleteFeed", ), - self.search_all_resources: self._wrap_method( + self.search_all_resources: self._wrap_async_method( self.search_all_resources, default_retry=retries.AsyncRetry( initial=0.1, @@ -1150,8 +1187,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=15.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/SearchAllResources", ), - self.search_all_iam_policies: self._wrap_method( + self.search_all_iam_policies: self._wrap_async_method( self.search_all_iam_policies, default_retry=retries.AsyncRetry( initial=0.1, @@ -1165,8 +1203,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=15.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/SearchAllIamPolicies", ), - self.analyze_iam_policy: self._wrap_method( + self.analyze_iam_policy: self._wrap_async_method( self.analyze_iam_policy, default_retry=retries.AsyncRetry( initial=0.1, @@ -1179,79 +1218,88 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=300.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeIamPolicy", ), - self.analyze_iam_policy_longrunning: self._wrap_method( + self.analyze_iam_policy_longrunning: self._wrap_async_method( self.analyze_iam_policy_longrunning, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeIamPolicyLongrunning", ), - self.analyze_move: self._wrap_method( + self.analyze_move: self._wrap_async_method( self.analyze_move, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeMove", ), - self.query_assets: self._wrap_method( + self.query_assets: self._wrap_async_method( self.query_assets, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/QueryAssets", ), - self.create_saved_query: self._wrap_method( + self.create_saved_query: self._wrap_async_method( self.create_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/CreateSavedQuery", ), - self.get_saved_query: self._wrap_method( + self.get_saved_query: self._wrap_async_method( self.get_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/GetSavedQuery", ), - self.list_saved_queries: self._wrap_method( + self.list_saved_queries: self._wrap_async_method( self.list_saved_queries, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/ListSavedQueries", ), - self.update_saved_query: self._wrap_method( + self.update_saved_query: self._wrap_async_method( self.update_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/UpdateSavedQuery", ), - self.delete_saved_query: self._wrap_method( + self.delete_saved_query: self._wrap_async_method( self.delete_saved_query, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/DeleteSavedQuery", ), - self.batch_get_effective_iam_policies: self._wrap_method( + self.batch_get_effective_iam_policies: self._wrap_async_method( self.batch_get_effective_iam_policies, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/BatchGetEffectiveIamPolicies", ), - self.analyze_org_policies: self._wrap_method( + self.analyze_org_policies: self._wrap_async_method( self.analyze_org_policies, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeOrgPolicies", ), - self.analyze_org_policy_governed_containers: self._wrap_method( + self.analyze_org_policy_governed_containers: self._wrap_async_method( self.analyze_org_policy_governed_containers, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeOrgPolicyGovernedContainers", ), - self.analyze_org_policy_governed_assets: self._wrap_method( + self.analyze_org_policy_governed_assets: self._wrap_async_method( self.analyze_org_policy_governed_assets, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeOrgPolicyGovernedAssets", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py index d85aa16473c2..a2dde35054eb 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.cloud.asset_v1._compat import transcode_request +from google.cloud.asset_v1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -1060,6 +1061,7 @@ class AssetServiceRestStub: _session: AuthorizedSession _host: str _interceptor: AssetServiceRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class AssetServiceRestTransport(_BaseAssetServiceRestTransport): @@ -1087,6 +1089,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[AssetServiceRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -1130,6 +1134,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -1141,7 +1148,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -1194,19 +1203,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.AnalyzeIamPolicyRequest, *, @@ -1272,7 +1296,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._AnalyzeIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._AnalyzeIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1284,7 +1317,6 @@ def __call__(self, pb_resp = asset_service.AnalyzeIamPolicyResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_analyze_iam_policy(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_analyze_iam_policy_with_metadata(resp, response_metadata) @@ -1321,20 +1353,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.AnalyzeIamPolicyLongrunningRequest, *, @@ -1402,7 +1449,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._AnalyzeIamPolicyLongrunning._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._AnalyzeIamPolicyLongrunning._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1412,7 +1469,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_analyze_iam_policy_longrunning(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_analyze_iam_policy_longrunning_with_metadata(resp, response_metadata) @@ -1449,19 +1505,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.AnalyzeMoveRequest, *, @@ -1527,7 +1598,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._AnalyzeMove._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._AnalyzeMove._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1539,7 +1619,6 @@ def __call__(self, pb_resp = asset_service.AnalyzeMoveResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_analyze_move(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_analyze_move_with_metadata(resp, response_metadata) @@ -1576,19 +1655,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.AnalyzeOrgPoliciesRequest, *, @@ -1654,7 +1748,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._AnalyzeOrgPolicies._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._AnalyzeOrgPolicies._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1666,7 +1769,6 @@ def __call__(self, pb_resp = asset_service.AnalyzeOrgPoliciesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_analyze_org_policies(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_analyze_org_policies_with_metadata(resp, response_metadata) @@ -1703,19 +1805,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.AnalyzeOrgPolicyGovernedAssetsRequest, *, @@ -1782,7 +1899,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._AnalyzeOrgPolicyGovernedAssets._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._AnalyzeOrgPolicyGovernedAssets._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1794,7 +1920,6 @@ def __call__(self, pb_resp = asset_service.AnalyzeOrgPolicyGovernedAssetsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_analyze_org_policy_governed_assets(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_analyze_org_policy_governed_assets_with_metadata(resp, response_metadata) @@ -1831,19 +1956,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.AnalyzeOrgPolicyGovernedContainersRequest, *, @@ -1910,7 +2050,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._AnalyzeOrgPolicyGovernedContainers._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._AnalyzeOrgPolicyGovernedContainers._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1922,7 +2071,6 @@ def __call__(self, pb_resp = asset_service.AnalyzeOrgPolicyGovernedContainersResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_analyze_org_policy_governed_containers(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_analyze_org_policy_governed_containers_with_metadata(resp, response_metadata) @@ -1959,19 +2107,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.BatchGetAssetsHistoryRequest, *, @@ -2034,7 +2197,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._BatchGetAssetsHistory._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._BatchGetAssetsHistory._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2046,7 +2218,6 @@ def __call__(self, pb_resp = asset_service.BatchGetAssetsHistoryResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_batch_get_assets_history(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_batch_get_assets_history_with_metadata(resp, response_metadata) @@ -2083,19 +2254,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.BatchGetEffectiveIamPoliciesRequest, *, @@ -2162,7 +2348,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._BatchGetEffectiveIamPolicies._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._BatchGetEffectiveIamPolicies._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2174,7 +2369,6 @@ def __call__(self, pb_resp = asset_service.BatchGetEffectiveIamPoliciesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_batch_get_effective_iam_policies(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_batch_get_effective_iam_policies_with_metadata(resp, response_metadata) @@ -2211,20 +2405,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.CreateFeedRequest, *, @@ -2295,7 +2504,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._CreateFeed._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._CreateFeed._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2307,7 +2526,6 @@ def __call__(self, pb_resp = asset_service.Feed.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_feed(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_feed_with_metadata(resp, response_metadata) @@ -2344,20 +2562,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.CreateSavedQueryRequest, *, @@ -2422,7 +2655,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._CreateSavedQuery._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._CreateSavedQuery._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2434,7 +2677,6 @@ def __call__(self, pb_resp = asset_service.SavedQuery.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_saved_query(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_saved_query_with_metadata(resp, response_metadata) @@ -2471,19 +2713,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.DeleteFeedRequest, *, @@ -2542,7 +2799,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._DeleteFeed._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._DeleteFeed._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2561,19 +2827,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.DeleteSavedQueryRequest, *, @@ -2632,7 +2913,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._DeleteSavedQuery._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._DeleteSavedQuery._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2651,20 +2941,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.ExportAssetsRequest, *, @@ -2730,7 +3035,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._ExportAssets._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._ExportAssets._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2740,7 +3055,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_export_assets(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_export_assets_with_metadata(resp, response_metadata) @@ -2777,19 +3091,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.GetFeedRequest, *, @@ -2860,7 +3189,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._GetFeed._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._GetFeed._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2872,7 +3210,6 @@ def __call__(self, pb_resp = asset_service.Feed.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_feed(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_feed_with_metadata(resp, response_metadata) @@ -2909,19 +3246,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.GetSavedQueryRequest, *, @@ -2986,7 +3338,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._GetSavedQuery._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._GetSavedQuery._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2998,7 +3359,6 @@ def __call__(self, pb_resp = asset_service.SavedQuery.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_saved_query(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_saved_query_with_metadata(resp, response_metadata) @@ -3035,19 +3395,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.ListAssetsRequest, *, @@ -3110,7 +3485,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._ListAssets._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._ListAssets._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3122,7 +3506,6 @@ def __call__(self, pb_resp = asset_service.ListAssetsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_assets(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_assets_with_metadata(resp, response_metadata) @@ -3159,19 +3542,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.ListFeedsRequest, *, @@ -3234,7 +3632,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._ListFeeds._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._ListFeeds._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3246,7 +3653,6 @@ def __call__(self, pb_resp = asset_service.ListFeedsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_feeds(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_feeds_with_metadata(resp, response_metadata) @@ -3283,19 +3689,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.ListSavedQueriesRequest, *, @@ -3358,7 +3779,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._ListSavedQueries._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._ListSavedQueries._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3370,7 +3800,6 @@ def __call__(self, pb_resp = asset_service.ListSavedQueriesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_saved_queries(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_saved_queries_with_metadata(resp, response_metadata) @@ -3407,20 +3836,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.QueryAssetsRequest, *, @@ -3483,7 +3927,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._QueryAssets._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._QueryAssets._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3495,7 +3949,6 @@ def __call__(self, pb_resp = asset_service.QueryAssetsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_query_assets(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_query_assets_with_metadata(resp, response_metadata) @@ -3532,19 +3985,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.SearchAllIamPoliciesRequest, *, @@ -3607,7 +4075,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._SearchAllIamPolicies._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._SearchAllIamPolicies._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3619,7 +4096,6 @@ def __call__(self, pb_resp = asset_service.SearchAllIamPoliciesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_search_all_iam_policies(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_search_all_iam_policies_with_metadata(resp, response_metadata) @@ -3656,19 +4132,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.SearchAllResourcesRequest, *, @@ -3731,7 +4222,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._SearchAllResources._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._SearchAllResources._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3743,7 +4243,6 @@ def __call__(self, pb_resp = asset_service.SearchAllResourcesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_search_all_resources(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_search_all_resources_with_metadata(resp, response_metadata) @@ -3780,20 +4279,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.UpdateFeedRequest, *, @@ -3864,7 +4378,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._UpdateFeed._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._UpdateFeed._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3876,7 +4400,6 @@ def __call__(self, pb_resp = asset_service.Feed.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_feed(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_feed_with_metadata(resp, response_metadata) @@ -3913,20 +4436,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: asset_service.UpdateSavedQueryRequest, *, @@ -3991,7 +4529,17 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._UpdateSavedQuery._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = AssetServiceRestTransport._UpdateSavedQuery._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4003,7 +4551,6 @@ def __call__(self, pb_resp = asset_service.SavedQuery.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_saved_query(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_saved_query_with_metadata(resp, response_metadata) @@ -4034,7 +4581,7 @@ def analyze_iam_policy(self) -> Callable[ asset_service.AnalyzeIamPolicyResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AnalyzeIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._AnalyzeIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def analyze_iam_policy_longrunning(self) -> Callable[ @@ -4042,7 +4589,7 @@ def analyze_iam_policy_longrunning(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AnalyzeIamPolicyLongrunning(self._session, self._host, self._interceptor) # type: ignore + return self._AnalyzeIamPolicyLongrunning(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def analyze_move(self) -> Callable[ @@ -4050,7 +4597,7 @@ def analyze_move(self) -> Callable[ asset_service.AnalyzeMoveResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AnalyzeMove(self._session, self._host, self._interceptor) # type: ignore + return self._AnalyzeMove(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def analyze_org_policies(self) -> Callable[ @@ -4058,7 +4605,7 @@ def analyze_org_policies(self) -> Callable[ asset_service.AnalyzeOrgPoliciesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AnalyzeOrgPolicies(self._session, self._host, self._interceptor) # type: ignore + return self._AnalyzeOrgPolicies(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def analyze_org_policy_governed_assets(self) -> Callable[ @@ -4066,7 +4613,7 @@ def analyze_org_policy_governed_assets(self) -> Callable[ asset_service.AnalyzeOrgPolicyGovernedAssetsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AnalyzeOrgPolicyGovernedAssets(self._session, self._host, self._interceptor) # type: ignore + return self._AnalyzeOrgPolicyGovernedAssets(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def analyze_org_policy_governed_containers(self) -> Callable[ @@ -4074,7 +4621,7 @@ def analyze_org_policy_governed_containers(self) -> Callable[ asset_service.AnalyzeOrgPolicyGovernedContainersResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AnalyzeOrgPolicyGovernedContainers(self._session, self._host, self._interceptor) # type: ignore + return self._AnalyzeOrgPolicyGovernedContainers(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def batch_get_assets_history(self) -> Callable[ @@ -4082,7 +4629,7 @@ def batch_get_assets_history(self) -> Callable[ asset_service.BatchGetAssetsHistoryResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._BatchGetAssetsHistory(self._session, self._host, self._interceptor) # type: ignore + return self._BatchGetAssetsHistory(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def batch_get_effective_iam_policies(self) -> Callable[ @@ -4090,7 +4637,7 @@ def batch_get_effective_iam_policies(self) -> Callable[ asset_service.BatchGetEffectiveIamPoliciesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._BatchGetEffectiveIamPolicies(self._session, self._host, self._interceptor) # type: ignore + return self._BatchGetEffectiveIamPolicies(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_feed(self) -> Callable[ @@ -4098,7 +4645,7 @@ def create_feed(self) -> Callable[ asset_service.Feed]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateFeed(self._session, self._host, self._interceptor) # type: ignore + return self._CreateFeed(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_saved_query(self) -> Callable[ @@ -4106,7 +4653,7 @@ def create_saved_query(self) -> Callable[ asset_service.SavedQuery]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateSavedQuery(self._session, self._host, self._interceptor) # type: ignore + return self._CreateSavedQuery(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_feed(self) -> Callable[ @@ -4114,7 +4661,7 @@ def delete_feed(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteFeed(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteFeed(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_saved_query(self) -> Callable[ @@ -4122,7 +4669,7 @@ def delete_saved_query(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteSavedQuery(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteSavedQuery(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def export_assets(self) -> Callable[ @@ -4130,7 +4677,7 @@ def export_assets(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ExportAssets(self._session, self._host, self._interceptor) # type: ignore + return self._ExportAssets(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_feed(self) -> Callable[ @@ -4138,7 +4685,7 @@ def get_feed(self) -> Callable[ asset_service.Feed]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetFeed(self._session, self._host, self._interceptor) # type: ignore + return self._GetFeed(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_saved_query(self) -> Callable[ @@ -4146,7 +4693,7 @@ def get_saved_query(self) -> Callable[ asset_service.SavedQuery]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetSavedQuery(self._session, self._host, self._interceptor) # type: ignore + return self._GetSavedQuery(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_assets(self) -> Callable[ @@ -4154,7 +4701,7 @@ def list_assets(self) -> Callable[ asset_service.ListAssetsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListAssets(self._session, self._host, self._interceptor) # type: ignore + return self._ListAssets(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_feeds(self) -> Callable[ @@ -4162,7 +4709,7 @@ def list_feeds(self) -> Callable[ asset_service.ListFeedsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListFeeds(self._session, self._host, self._interceptor) # type: ignore + return self._ListFeeds(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_saved_queries(self) -> Callable[ @@ -4170,7 +4717,7 @@ def list_saved_queries(self) -> Callable[ asset_service.ListSavedQueriesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListSavedQueries(self._session, self._host, self._interceptor) # type: ignore + return self._ListSavedQueries(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def query_assets(self) -> Callable[ @@ -4178,7 +4725,7 @@ def query_assets(self) -> Callable[ asset_service.QueryAssetsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._QueryAssets(self._session, self._host, self._interceptor) # type: ignore + return self._QueryAssets(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def search_all_iam_policies(self) -> Callable[ @@ -4186,7 +4733,7 @@ def search_all_iam_policies(self) -> Callable[ asset_service.SearchAllIamPoliciesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._SearchAllIamPolicies(self._session, self._host, self._interceptor) # type: ignore + return self._SearchAllIamPolicies(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def search_all_resources(self) -> Callable[ @@ -4194,7 +4741,7 @@ def search_all_resources(self) -> Callable[ asset_service.SearchAllResourcesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._SearchAllResources(self._session, self._host, self._interceptor) # type: ignore + return self._SearchAllResources(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_feed(self) -> Callable[ @@ -4202,7 +4749,7 @@ def update_feed(self) -> Callable[ asset_service.Feed]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateFeed(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateFeed(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_saved_query(self) -> Callable[ @@ -4210,11 +4757,11 @@ def update_saved_query(self) -> Callable[ asset_service.SavedQuery]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateSavedQuery(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateSavedQuery(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseAssetServiceRestTransport._BaseGetOperation, AssetServiceRestStub): def __hash__(self): @@ -4228,19 +4775,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -4303,7 +4865,16 @@ def __call__(self, ) # Send the request - response = AssetServiceRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = AssetServiceRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest_base.py index b5b30671d1fb..d79cf2f07000 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from .base import AssetServiceTransport, DEFAULT_CLIENT_INFO @@ -49,6 +50,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -69,6 +72,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -84,7 +90,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseAnalyzeIamPolicy: diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index e86b23c549e4..ea507dee5887 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -56,6 +56,7 @@ from google.cloud.asset_v1.services.asset_service import AssetServiceClient from google.cloud.asset_v1.services.asset_service import pagers from google.cloud.asset_v1.services.asset_service import transports +from google.cloud.asset_v1.services.asset_service.client import _observability from google.cloud.asset_v1.types import asset_service from google.cloud.asset_v1.types import assets from google.longrunning import operations_pb2 # type: ignore @@ -760,6 +761,192 @@ def test_asset_service_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_asset_service_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.AssetServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = AssetServiceClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_asset_service_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.AssetServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.AssetServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_asset_service_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.AssetServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.AssetServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_asset_service_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.AssetServiceGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_asset_service_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.AssetServiceGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.AssetServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.AssetServiceGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.AssetServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_asset_service_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.AssetServiceGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.AssetServiceGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (AssetServiceClient, transports.AssetServiceGrpcTransport, "grpc", grpc_helpers), (AssetServiceAsyncClient, transports.AssetServiceGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py index df64f9d08916..68bbe8b85472 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.cloud.asset_v1._compat import transcode_request -from google.cloud.asset_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.asset_v1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.asset_v1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.asset_v1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py index da065db5907b..f72f88247061 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.iam.credentials_v1._compat import _observability from google.iam.credentials_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -482,18 +483,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., IAMCredentialsTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py index 37bcbf2cb766..5d577415bdad 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.iam.credentials_v1 import gapic_version as package_version +from google.iam.credentials_v1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -52,6 +59,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -82,6 +90,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -119,16 +130,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.generate_access_token: gapic_v1.method.wrap_method( + self.generate_access_token: self._wrap_method( self.generate_access_token, default_retry=retries.Retry( initial=0.1, @@ -142,8 +204,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/GenerateAccessToken", ), - self.generate_id_token: gapic_v1.method.wrap_method( + self.generate_id_token: self._wrap_method( self.generate_id_token, default_retry=retries.Retry( initial=0.1, @@ -157,8 +220,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/GenerateIdToken", ), - self.sign_blob: gapic_v1.method.wrap_method( + self.sign_blob: self._wrap_method( self.sign_blob, default_retry=retries.Retry( initial=0.1, @@ -172,8 +236,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/SignBlob", ), - self.sign_jwt: gapic_v1.method.wrap_method( + self.sign_jwt: self._wrap_method( self.sign_jwt, default_retry=retries.Retry( initial=0.1, @@ -187,6 +252,7 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/SignJwt", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py index 18428ad7d6e0..31fbf17d91cb 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.iam.credentials_v1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -33,6 +37,10 @@ from google.iam.credentials_v1.types import common from .base import IAMCredentialsTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -138,6 +146,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -188,6 +198,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -243,6 +259,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -264,8 +281,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py index d9d401f8d9f1..19603cc28836 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.iam.credentials_v1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -37,6 +40,7 @@ from .base import IAMCredentialsTransport, DEFAULT_CLIENT_INFO from .grpc import IAMCredentialsGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -185,6 +189,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -236,6 +242,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -290,6 +302,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -311,10 +324,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -437,9 +463,12 @@ def sign_jwt(self) -> Callable[ return self._stubs['sign_jwt'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.generate_access_token: self._wrap_method( + self.generate_access_token: self._wrap_async_method( self.generate_access_token, default_retry=retries.AsyncRetry( initial=0.1, @@ -453,8 +482,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/GenerateAccessToken", ), - self.generate_id_token: self._wrap_method( + self.generate_id_token: self._wrap_async_method( self.generate_id_token, default_retry=retries.AsyncRetry( initial=0.1, @@ -468,8 +498,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/GenerateIdToken", ), - self.sign_blob: self._wrap_method( + self.sign_blob: self._wrap_async_method( self.sign_blob, default_retry=retries.AsyncRetry( initial=0.1, @@ -483,8 +514,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/SignBlob", ), - self.sign_jwt: self._wrap_method( + self.sign_jwt: self._wrap_async_method( self.sign_jwt, default_retry=retries.AsyncRetry( initial=0.1, @@ -498,14 +530,10 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/SignJwt", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py index 0cffb09641ed..fff3c34c4c26 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.iam.credentials_v1._compat import transcode_request +from google.iam.credentials_v1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -264,6 +265,7 @@ class IAMCredentialsRestStub: _session: AuthorizedSession _host: str _interceptor: IAMCredentialsRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class IAMCredentialsRestTransport(_BaseIAMCredentialsRestTransport): @@ -300,6 +302,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[IAMCredentialsRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -343,6 +347,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -354,7 +361,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -375,20 +384,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: common.GenerateAccessTokenRequest, *, @@ -451,7 +475,17 @@ def __call__(self, ) # Send the request - response = IAMCredentialsRestTransport._GenerateAccessToken._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IAMCredentialsRestTransport._GenerateAccessToken._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -463,7 +497,6 @@ def __call__(self, pb_resp = common.GenerateAccessTokenResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_generate_access_token(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_generate_access_token_with_metadata(resp, response_metadata) @@ -500,20 +533,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: common.GenerateIdTokenRequest, *, @@ -576,7 +624,17 @@ def __call__(self, ) # Send the request - response = IAMCredentialsRestTransport._GenerateIdToken._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IAMCredentialsRestTransport._GenerateIdToken._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -588,7 +646,6 @@ def __call__(self, pb_resp = common.GenerateIdTokenResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_generate_id_token(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_generate_id_token_with_metadata(resp, response_metadata) @@ -625,20 +682,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: common.SignBlobRequest, *, @@ -701,7 +773,17 @@ def __call__(self, ) # Send the request - response = IAMCredentialsRestTransport._SignBlob._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IAMCredentialsRestTransport._SignBlob._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -713,7 +795,6 @@ def __call__(self, pb_resp = common.SignBlobResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_sign_blob(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_sign_blob_with_metadata(resp, response_metadata) @@ -750,20 +831,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: common.SignJwtRequest, *, @@ -826,7 +922,17 @@ def __call__(self, ) # Send the request - response = IAMCredentialsRestTransport._SignJwt._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IAMCredentialsRestTransport._SignJwt._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -838,7 +944,6 @@ def __call__(self, pb_resp = common.SignJwtResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_sign_jwt(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_sign_jwt_with_metadata(resp, response_metadata) @@ -869,7 +974,7 @@ def generate_access_token(self) -> Callable[ common.GenerateAccessTokenResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GenerateAccessToken(self._session, self._host, self._interceptor) # type: ignore + return self._GenerateAccessToken(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def generate_id_token(self) -> Callable[ @@ -877,7 +982,7 @@ def generate_id_token(self) -> Callable[ common.GenerateIdTokenResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GenerateIdToken(self._session, self._host, self._interceptor) # type: ignore + return self._GenerateIdToken(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def sign_blob(self) -> Callable[ @@ -885,7 +990,7 @@ def sign_blob(self) -> Callable[ common.SignBlobResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._SignBlob(self._session, self._host, self._interceptor) # type: ignore + return self._SignBlob(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def sign_jwt(self) -> Callable[ @@ -893,7 +998,7 @@ def sign_jwt(self) -> Callable[ common.SignJwtResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._SignJwt(self._session, self._host, self._interceptor) # type: ignore + return self._SignJwt(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def kind(self) -> str: diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest_base.py index 3c578f3f8485..3e11472082ab 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from .base import IAMCredentialsTransport, DEFAULT_CLIENT_INFO @@ -47,6 +48,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -67,6 +70,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -82,7 +88,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseGenerateAccessToken: diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py index 066f4505bdef..fbabb8248199 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.iam.credentials_v1._compat import transcode_request -from google.iam.credentials_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.iam.credentials_v1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.iam.credentials_v1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.iam.credentials_v1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index a13fa010afd5..d374eae044ac 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -52,6 +52,7 @@ from google.iam.credentials_v1.services.iam_credentials import IAMCredentialsAsyncClient from google.iam.credentials_v1.services.iam_credentials import IAMCredentialsClient from google.iam.credentials_v1.services.iam_credentials import transports +from google.iam.credentials_v1.services.iam_credentials.client import _observability from google.iam.credentials_v1.types import common from google.oauth2 import service_account import google.auth @@ -750,6 +751,192 @@ def test_iam_credentials_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_iam_credentials_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.IAMCredentialsGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = IAMCredentialsClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_iam_credentials_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.IAMCredentialsGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.IAMCredentialsGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_iam_credentials_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.IAMCredentialsGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.IAMCredentialsGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_iam_credentials_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.IAMCredentialsGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_iam_credentials_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.IAMCredentialsGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.IAMCredentialsGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.IAMCredentialsGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.IAMCredentialsGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_iam_credentials_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.IAMCredentialsGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.IAMCredentialsGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (IAMCredentialsClient, transports.IAMCredentialsGrpcTransport, "grpc", grpc_helpers), (IAMCredentialsAsyncClient, transports.IAMCredentialsGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py index f5442cba6179..205b75d4a78c 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.eventarc_v1._compat import _observability from google.cloud.eventarc_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -665,18 +666,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., EventarcTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py index 3c054d084716..9aac532d8376 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.eventarc_v1 import gapic_version as package_version +from google.cloud.eventarc_v1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -67,6 +74,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -97,6 +105,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -134,254 +145,353 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.get_trigger: gapic_v1.method.wrap_method( + self.get_trigger: self._wrap_method( self.get_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetTrigger", ), - self.list_triggers: gapic_v1.method.wrap_method( + self.list_triggers: self._wrap_method( self.list_triggers, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListTriggers", ), - self.create_trigger: gapic_v1.method.wrap_method( + self.create_trigger: self._wrap_method( self.create_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateTrigger", ), - self.update_trigger: gapic_v1.method.wrap_method( + self.update_trigger: self._wrap_method( self.update_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateTrigger", ), - self.delete_trigger: gapic_v1.method.wrap_method( + self.delete_trigger: self._wrap_method( self.delete_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteTrigger", ), - self.get_channel: gapic_v1.method.wrap_method( + self.get_channel: self._wrap_method( self.get_channel, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetChannel", ), - self.list_channels: gapic_v1.method.wrap_method( + self.list_channels: self._wrap_method( self.list_channels, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListChannels", ), - self.create_channel_: gapic_v1.method.wrap_method( + self.create_channel_: self._wrap_method( self.create_channel_, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateChannel", ), - self.update_channel: gapic_v1.method.wrap_method( + self.update_channel: self._wrap_method( self.update_channel, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateChannel", ), - self.delete_channel: gapic_v1.method.wrap_method( + self.delete_channel: self._wrap_method( self.delete_channel, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteChannel", ), - self.get_provider: gapic_v1.method.wrap_method( + self.get_provider: self._wrap_method( self.get_provider, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetProvider", ), - self.list_providers: gapic_v1.method.wrap_method( + self.list_providers: self._wrap_method( self.list_providers, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListProviders", ), - self.get_channel_connection: gapic_v1.method.wrap_method( + self.get_channel_connection: self._wrap_method( self.get_channel_connection, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetChannelConnection", ), - self.list_channel_connections: gapic_v1.method.wrap_method( + self.list_channel_connections: self._wrap_method( self.list_channel_connections, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListChannelConnections", ), - self.create_channel_connection: gapic_v1.method.wrap_method( + self.create_channel_connection: self._wrap_method( self.create_channel_connection, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateChannelConnection", ), - self.delete_channel_connection: gapic_v1.method.wrap_method( + self.delete_channel_connection: self._wrap_method( self.delete_channel_connection, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteChannelConnection", ), - self.get_google_channel_config: gapic_v1.method.wrap_method( + self.get_google_channel_config: self._wrap_method( self.get_google_channel_config, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetGoogleChannelConfig", ), - self.update_google_channel_config: gapic_v1.method.wrap_method( + self.update_google_channel_config: self._wrap_method( self.update_google_channel_config, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateGoogleChannelConfig", ), - self.get_message_bus: gapic_v1.method.wrap_method( + self.get_message_bus: self._wrap_method( self.get_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetMessageBus", ), - self.list_message_buses: gapic_v1.method.wrap_method( + self.list_message_buses: self._wrap_method( self.list_message_buses, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListMessageBuses", ), - self.list_message_bus_enrollments: gapic_v1.method.wrap_method( + self.list_message_bus_enrollments: self._wrap_method( self.list_message_bus_enrollments, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListMessageBusEnrollments", ), - self.create_message_bus: gapic_v1.method.wrap_method( + self.create_message_bus: self._wrap_method( self.create_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateMessageBus", ), - self.update_message_bus: gapic_v1.method.wrap_method( + self.update_message_bus: self._wrap_method( self.update_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateMessageBus", ), - self.delete_message_bus: gapic_v1.method.wrap_method( + self.delete_message_bus: self._wrap_method( self.delete_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteMessageBus", ), - self.get_enrollment: gapic_v1.method.wrap_method( + self.get_enrollment: self._wrap_method( self.get_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetEnrollment", ), - self.list_enrollments: gapic_v1.method.wrap_method( + self.list_enrollments: self._wrap_method( self.list_enrollments, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListEnrollments", ), - self.create_enrollment: gapic_v1.method.wrap_method( + self.create_enrollment: self._wrap_method( self.create_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateEnrollment", ), - self.update_enrollment: gapic_v1.method.wrap_method( + self.update_enrollment: self._wrap_method( self.update_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateEnrollment", ), - self.delete_enrollment: gapic_v1.method.wrap_method( + self.delete_enrollment: self._wrap_method( self.delete_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteEnrollment", ), - self.get_pipeline: gapic_v1.method.wrap_method( + self.get_pipeline: self._wrap_method( self.get_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetPipeline", ), - self.list_pipelines: gapic_v1.method.wrap_method( + self.list_pipelines: self._wrap_method( self.list_pipelines, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListPipelines", ), - self.create_pipeline: gapic_v1.method.wrap_method( + self.create_pipeline: self._wrap_method( self.create_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreatePipeline", ), - self.update_pipeline: gapic_v1.method.wrap_method( + self.update_pipeline: self._wrap_method( self.update_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdatePipeline", ), - self.delete_pipeline: gapic_v1.method.wrap_method( + self.delete_pipeline: self._wrap_method( self.delete_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeletePipeline", ), - self.get_google_api_source: gapic_v1.method.wrap_method( + self.get_google_api_source: self._wrap_method( self.get_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetGoogleApiSource", ), - self.list_google_api_sources: gapic_v1.method.wrap_method( + self.list_google_api_sources: self._wrap_method( self.list_google_api_sources, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListGoogleApiSources", ), - self.create_google_api_source: gapic_v1.method.wrap_method( + self.create_google_api_source: self._wrap_method( self.create_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateGoogleApiSource", ), - self.update_google_api_source: gapic_v1.method.wrap_method( + self.update_google_api_source: self._wrap_method( self.update_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateGoogleApiSource", ), - self.delete_google_api_source: gapic_v1.method.wrap_method( + self.delete_google_api_source: self._wrap_method( self.delete_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteGoogleApiSource", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py index ac5d9a0fbe92..f4cf2bfdbaff 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.eventarc_v1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -48,6 +52,10 @@ from google.longrunning import operations_pb2 # type: ignore from .base import EventarcTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -146,6 +154,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -196,6 +206,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -252,6 +268,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -273,8 +290,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py index 966a52b3d9dd..d350d3d2d3c0 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.eventarc_v1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -52,6 +55,7 @@ from .base import EventarcTransport, DEFAULT_CLIENT_INFO from .grpc import EventarcGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -193,6 +197,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -244,6 +250,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -299,6 +311,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -320,10 +333,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -1377,255 +1403,301 @@ def delete_google_api_source(self) -> Callable[ return self._stubs['delete_google_api_source'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.get_trigger: self._wrap_method( + self.get_trigger: self._wrap_async_method( self.get_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetTrigger", ), - self.list_triggers: self._wrap_method( + self.list_triggers: self._wrap_async_method( self.list_triggers, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListTriggers", ), - self.create_trigger: self._wrap_method( + self.create_trigger: self._wrap_async_method( self.create_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateTrigger", ), - self.update_trigger: self._wrap_method( + self.update_trigger: self._wrap_async_method( self.update_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateTrigger", ), - self.delete_trigger: self._wrap_method( + self.delete_trigger: self._wrap_async_method( self.delete_trigger, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteTrigger", ), - self.get_channel: self._wrap_method( + self.get_channel: self._wrap_async_method( self.get_channel, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetChannel", ), - self.list_channels: self._wrap_method( + self.list_channels: self._wrap_async_method( self.list_channels, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListChannels", ), - self.create_channel_: self._wrap_method( + self.create_channel_: self._wrap_async_method( self.create_channel_, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateChannel", ), - self.update_channel: self._wrap_method( + self.update_channel: self._wrap_async_method( self.update_channel, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateChannel", ), - self.delete_channel: self._wrap_method( + self.delete_channel: self._wrap_async_method( self.delete_channel, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteChannel", ), - self.get_provider: self._wrap_method( + self.get_provider: self._wrap_async_method( self.get_provider, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetProvider", ), - self.list_providers: self._wrap_method( + self.list_providers: self._wrap_async_method( self.list_providers, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListProviders", ), - self.get_channel_connection: self._wrap_method( + self.get_channel_connection: self._wrap_async_method( self.get_channel_connection, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetChannelConnection", ), - self.list_channel_connections: self._wrap_method( + self.list_channel_connections: self._wrap_async_method( self.list_channel_connections, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListChannelConnections", ), - self.create_channel_connection: self._wrap_method( + self.create_channel_connection: self._wrap_async_method( self.create_channel_connection, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateChannelConnection", ), - self.delete_channel_connection: self._wrap_method( + self.delete_channel_connection: self._wrap_async_method( self.delete_channel_connection, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteChannelConnection", ), - self.get_google_channel_config: self._wrap_method( + self.get_google_channel_config: self._wrap_async_method( self.get_google_channel_config, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetGoogleChannelConfig", ), - self.update_google_channel_config: self._wrap_method( + self.update_google_channel_config: self._wrap_async_method( self.update_google_channel_config, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateGoogleChannelConfig", ), - self.get_message_bus: self._wrap_method( + self.get_message_bus: self._wrap_async_method( self.get_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetMessageBus", ), - self.list_message_buses: self._wrap_method( + self.list_message_buses: self._wrap_async_method( self.list_message_buses, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListMessageBuses", ), - self.list_message_bus_enrollments: self._wrap_method( + self.list_message_bus_enrollments: self._wrap_async_method( self.list_message_bus_enrollments, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListMessageBusEnrollments", ), - self.create_message_bus: self._wrap_method( + self.create_message_bus: self._wrap_async_method( self.create_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateMessageBus", ), - self.update_message_bus: self._wrap_method( + self.update_message_bus: self._wrap_async_method( self.update_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateMessageBus", ), - self.delete_message_bus: self._wrap_method( + self.delete_message_bus: self._wrap_async_method( self.delete_message_bus, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteMessageBus", ), - self.get_enrollment: self._wrap_method( + self.get_enrollment: self._wrap_async_method( self.get_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetEnrollment", ), - self.list_enrollments: self._wrap_method( + self.list_enrollments: self._wrap_async_method( self.list_enrollments, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListEnrollments", ), - self.create_enrollment: self._wrap_method( + self.create_enrollment: self._wrap_async_method( self.create_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateEnrollment", ), - self.update_enrollment: self._wrap_method( + self.update_enrollment: self._wrap_async_method( self.update_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateEnrollment", ), - self.delete_enrollment: self._wrap_method( + self.delete_enrollment: self._wrap_async_method( self.delete_enrollment, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteEnrollment", ), - self.get_pipeline: self._wrap_method( + self.get_pipeline: self._wrap_async_method( self.get_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetPipeline", ), - self.list_pipelines: self._wrap_method( + self.list_pipelines: self._wrap_async_method( self.list_pipelines, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListPipelines", ), - self.create_pipeline: self._wrap_method( + self.create_pipeline: self._wrap_async_method( self.create_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreatePipeline", ), - self.update_pipeline: self._wrap_method( + self.update_pipeline: self._wrap_async_method( self.update_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdatePipeline", ), - self.delete_pipeline: self._wrap_method( + self.delete_pipeline: self._wrap_async_method( self.delete_pipeline, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeletePipeline", ), - self.get_google_api_source: self._wrap_method( + self.get_google_api_source: self._wrap_async_method( self.get_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetGoogleApiSource", ), - self.list_google_api_sources: self._wrap_method( + self.list_google_api_sources: self._wrap_async_method( self.list_google_api_sources, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListGoogleApiSources", ), - self.create_google_api_source: self._wrap_method( + self.create_google_api_source: self._wrap_async_method( self.create_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateGoogleApiSource", ), - self.update_google_api_source: self._wrap_method( + self.update_google_api_source: self._wrap_async_method( self.update_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/UpdateGoogleApiSource", ), - self.delete_google_api_source: self._wrap_method( + self.delete_google_api_source: self._wrap_async_method( self.delete_google_api_source, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteGoogleApiSource", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py index 1565671cf8d4..f25dd893e29e 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.cloud.eventarc_v1._compat import transcode_request +from google.cloud.eventarc_v1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -2008,6 +2009,7 @@ class EventarcRestStub: _session: AuthorizedSession _host: str _interceptor: EventarcRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class EventarcRestTransport(_BaseEventarcRestTransport): @@ -2037,6 +2039,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[EventarcRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -2080,6 +2084,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -2091,7 +2098,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -2163,20 +2172,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreateChannelRequest, *, @@ -2243,7 +2267,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreateChannel._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreateChannel._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2253,7 +2287,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_channel(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_channel_with_metadata(resp, response_metadata) @@ -2290,20 +2323,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreateChannelConnectionRequest, *, @@ -2370,7 +2418,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreateChannelConnection._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreateChannelConnection._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2380,7 +2438,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_channel_connection(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_channel_connection_with_metadata(resp, response_metadata) @@ -2417,20 +2474,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreateEnrollmentRequest, *, @@ -2497,7 +2569,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreateEnrollment._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreateEnrollment._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2507,7 +2589,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_enrollment(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_enrollment_with_metadata(resp, response_metadata) @@ -2544,20 +2625,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreateGoogleApiSourceRequest, *, @@ -2624,7 +2720,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreateGoogleApiSource._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreateGoogleApiSource._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2634,7 +2740,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_google_api_source(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_google_api_source_with_metadata(resp, response_metadata) @@ -2671,20 +2776,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreateMessageBusRequest, *, @@ -2751,7 +2871,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreateMessageBus._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreateMessageBus._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2761,7 +2891,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_message_bus(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_message_bus_with_metadata(resp, response_metadata) @@ -2798,20 +2927,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreatePipelineRequest, *, @@ -2878,7 +3022,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreatePipeline._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreatePipeline._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2888,7 +3042,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_pipeline(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_pipeline_with_metadata(resp, response_metadata) @@ -2925,20 +3078,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.CreateTriggerRequest, *, @@ -3005,7 +3173,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CreateTrigger._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CreateTrigger._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3015,7 +3193,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_trigger(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_trigger_with_metadata(resp, response_metadata) @@ -3052,19 +3229,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeleteChannelRequest, *, @@ -3131,7 +3323,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteChannel._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteChannel._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3141,7 +3342,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_channel(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_channel_with_metadata(resp, response_metadata) @@ -3178,19 +3378,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeleteChannelConnectionRequest, *, @@ -3257,7 +3472,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteChannelConnection._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteChannelConnection._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3267,7 +3491,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_channel_connection(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_channel_connection_with_metadata(resp, response_metadata) @@ -3304,19 +3527,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeleteEnrollmentRequest, *, @@ -3383,7 +3621,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteEnrollment._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteEnrollment._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3393,7 +3640,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_enrollment(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_enrollment_with_metadata(resp, response_metadata) @@ -3430,19 +3676,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeleteGoogleApiSourceRequest, *, @@ -3509,7 +3770,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteGoogleApiSource._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteGoogleApiSource._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3519,7 +3789,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_google_api_source(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_google_api_source_with_metadata(resp, response_metadata) @@ -3556,19 +3825,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeleteMessageBusRequest, *, @@ -3635,7 +3919,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteMessageBus._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteMessageBus._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3645,7 +3938,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_message_bus(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_message_bus_with_metadata(resp, response_metadata) @@ -3682,19 +3974,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeletePipelineRequest, *, @@ -3761,7 +4068,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeletePipeline._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeletePipeline._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3771,7 +4087,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_pipeline(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_pipeline_with_metadata(resp, response_metadata) @@ -3808,19 +4123,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.DeleteTriggerRequest, *, @@ -3887,7 +4217,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteTrigger._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteTrigger._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3897,7 +4236,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_trigger(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_trigger_with_metadata(resp, response_metadata) @@ -3934,19 +4272,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetChannelRequest, *, @@ -4018,7 +4371,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetChannel._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetChannel._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4030,7 +4392,6 @@ def __call__(self, pb_resp = channel.Channel.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_channel(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_channel_with_metadata(resp, response_metadata) @@ -4067,19 +4428,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetChannelConnectionRequest, *, @@ -4150,7 +4526,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetChannelConnection._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetChannelConnection._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4162,7 +4547,6 @@ def __call__(self, pb_resp = channel_connection.ChannelConnection.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_channel_connection(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_channel_connection_with_metadata(resp, response_metadata) @@ -4199,19 +4583,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetEnrollmentRequest, *, @@ -4281,7 +4680,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetEnrollment._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetEnrollment._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4293,7 +4701,6 @@ def __call__(self, pb_resp = enrollment.Enrollment.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_enrollment(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_enrollment_with_metadata(resp, response_metadata) @@ -4330,19 +4737,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetGoogleApiSourceRequest, *, @@ -4409,7 +4831,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetGoogleApiSource._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetGoogleApiSource._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4421,7 +4852,6 @@ def __call__(self, pb_resp = google_api_source.GoogleApiSource.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_google_api_source(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_google_api_source_with_metadata(resp, response_metadata) @@ -4458,19 +4888,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetGoogleChannelConfigRequest, *, @@ -4542,7 +4987,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetGoogleChannelConfig._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetGoogleChannelConfig._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4554,7 +5008,6 @@ def __call__(self, pb_resp = google_channel_config.GoogleChannelConfig.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_google_channel_config(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_google_channel_config_with_metadata(resp, response_metadata) @@ -4591,19 +5044,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetMessageBusRequest, *, @@ -4675,7 +5143,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetMessageBus._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetMessageBus._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4687,7 +5164,6 @@ def __call__(self, pb_resp = message_bus.MessageBus.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_message_bus(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_message_bus_with_metadata(resp, response_metadata) @@ -4724,19 +5200,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetPipelineRequest, *, @@ -4802,7 +5293,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetPipeline._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetPipeline._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4814,7 +5314,6 @@ def __call__(self, pb_resp = pipeline.Pipeline.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_pipeline(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_pipeline_with_metadata(resp, response_metadata) @@ -4851,19 +5350,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetProviderRequest, *, @@ -4929,7 +5443,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetProvider._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetProvider._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4941,7 +5464,6 @@ def __call__(self, pb_resp = discovery.Provider.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_provider(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_provider_with_metadata(resp, response_metadata) @@ -4978,19 +5500,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.GetTriggerRequest, *, @@ -5056,7 +5593,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetTrigger._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetTrigger._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5068,7 +5614,6 @@ def __call__(self, pb_resp = trigger.Trigger.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_trigger(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_trigger_with_metadata(resp, response_metadata) @@ -5105,19 +5650,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListChannelConnectionsRequest, *, @@ -5183,7 +5743,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListChannelConnections._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListChannelConnections._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5195,7 +5764,6 @@ def __call__(self, pb_resp = eventarc.ListChannelConnectionsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_channel_connections(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_channel_connections_with_metadata(resp, response_metadata) @@ -5232,19 +5800,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListChannelsRequest, *, @@ -5308,7 +5891,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListChannels._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListChannels._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5320,7 +5912,6 @@ def __call__(self, pb_resp = eventarc.ListChannelsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_channels(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_channels_with_metadata(resp, response_metadata) @@ -5357,19 +5948,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListEnrollmentsRequest, *, @@ -5433,7 +6039,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListEnrollments._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListEnrollments._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5445,7 +6060,6 @@ def __call__(self, pb_resp = eventarc.ListEnrollmentsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_enrollments(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_enrollments_with_metadata(resp, response_metadata) @@ -5482,19 +6096,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListGoogleApiSourcesRequest, *, @@ -5560,7 +6189,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListGoogleApiSources._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListGoogleApiSources._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5572,7 +6210,6 @@ def __call__(self, pb_resp = eventarc.ListGoogleApiSourcesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_google_api_sources(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_google_api_sources_with_metadata(resp, response_metadata) @@ -5609,19 +6246,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListMessageBusEnrollmentsRequest, *, @@ -5688,7 +6340,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListMessageBusEnrollments._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListMessageBusEnrollments._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5700,7 +6361,6 @@ def __call__(self, pb_resp = eventarc.ListMessageBusEnrollmentsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_message_bus_enrollments(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_message_bus_enrollments_with_metadata(resp, response_metadata) @@ -5737,19 +6397,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListMessageBusesRequest, *, @@ -5815,7 +6490,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListMessageBuses._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListMessageBuses._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5827,7 +6511,6 @@ def __call__(self, pb_resp = eventarc.ListMessageBusesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_message_buses(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_message_buses_with_metadata(resp, response_metadata) @@ -5864,19 +6547,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListPipelinesRequest, *, @@ -5942,7 +6640,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListPipelines._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListPipelines._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5954,7 +6661,6 @@ def __call__(self, pb_resp = eventarc.ListPipelinesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_pipelines(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_pipelines_with_metadata(resp, response_metadata) @@ -5991,19 +6697,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListProvidersRequest, *, @@ -6067,7 +6788,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListProviders._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListProviders._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6079,7 +6809,6 @@ def __call__(self, pb_resp = eventarc.ListProvidersResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_providers(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_providers_with_metadata(resp, response_metadata) @@ -6116,19 +6845,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.ListTriggersRequest, *, @@ -6192,7 +6936,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListTriggers._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListTriggers._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6204,7 +6957,6 @@ def __call__(self, pb_resp = eventarc.ListTriggersResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_triggers(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_triggers_with_metadata(resp, response_metadata) @@ -6241,20 +6993,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdateChannelRequest, *, @@ -6321,7 +7088,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdateChannel._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdateChannel._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6331,7 +7108,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_channel(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_channel_with_metadata(resp, response_metadata) @@ -6368,20 +7144,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdateEnrollmentRequest, *, @@ -6448,7 +7239,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdateEnrollment._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdateEnrollment._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6458,7 +7259,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_enrollment(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_enrollment_with_metadata(resp, response_metadata) @@ -6495,20 +7295,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdateGoogleApiSourceRequest, *, @@ -6575,7 +7390,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdateGoogleApiSource._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdateGoogleApiSource._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6585,7 +7410,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_google_api_source(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_google_api_source_with_metadata(resp, response_metadata) @@ -6622,20 +7446,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdateGoogleChannelConfigRequest, *, @@ -6708,7 +7547,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdateGoogleChannelConfig._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdateGoogleChannelConfig._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6720,7 +7569,6 @@ def __call__(self, pb_resp = gce_google_channel_config.GoogleChannelConfig.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_google_channel_config(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_google_channel_config_with_metadata(resp, response_metadata) @@ -6757,20 +7605,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdateMessageBusRequest, *, @@ -6837,7 +7700,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdateMessageBus._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdateMessageBus._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6847,7 +7720,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_message_bus(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_message_bus_with_metadata(resp, response_metadata) @@ -6884,20 +7756,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdatePipelineRequest, *, @@ -6964,7 +7851,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdatePipeline._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdatePipeline._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6974,7 +7871,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_pipeline(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_pipeline_with_metadata(resp, response_metadata) @@ -7011,20 +7907,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: eventarc.UpdateTriggerRequest, *, @@ -7091,7 +8002,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._UpdateTrigger._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._UpdateTrigger._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7101,7 +8022,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_trigger(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_trigger_with_metadata(resp, response_metadata) @@ -7132,7 +8052,7 @@ def create_channel_(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateChannel(self._session, self._host, self._interceptor) # type: ignore + return self._CreateChannel(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_channel_connection(self) -> Callable[ @@ -7140,7 +8060,7 @@ def create_channel_connection(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateChannelConnection(self._session, self._host, self._interceptor) # type: ignore + return self._CreateChannelConnection(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_enrollment(self) -> Callable[ @@ -7148,7 +8068,7 @@ def create_enrollment(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateEnrollment(self._session, self._host, self._interceptor) # type: ignore + return self._CreateEnrollment(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_google_api_source(self) -> Callable[ @@ -7156,7 +8076,7 @@ def create_google_api_source(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateGoogleApiSource(self._session, self._host, self._interceptor) # type: ignore + return self._CreateGoogleApiSource(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_message_bus(self) -> Callable[ @@ -7164,7 +8084,7 @@ def create_message_bus(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateMessageBus(self._session, self._host, self._interceptor) # type: ignore + return self._CreateMessageBus(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_pipeline(self) -> Callable[ @@ -7172,7 +8092,7 @@ def create_pipeline(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreatePipeline(self._session, self._host, self._interceptor) # type: ignore + return self._CreatePipeline(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_trigger(self) -> Callable[ @@ -7180,7 +8100,7 @@ def create_trigger(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateTrigger(self._session, self._host, self._interceptor) # type: ignore + return self._CreateTrigger(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_channel(self) -> Callable[ @@ -7188,7 +8108,7 @@ def delete_channel(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteChannel(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteChannel(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_channel_connection(self) -> Callable[ @@ -7196,7 +8116,7 @@ def delete_channel_connection(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteChannelConnection(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteChannelConnection(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_enrollment(self) -> Callable[ @@ -7204,7 +8124,7 @@ def delete_enrollment(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteEnrollment(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteEnrollment(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_google_api_source(self) -> Callable[ @@ -7212,7 +8132,7 @@ def delete_google_api_source(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteGoogleApiSource(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteGoogleApiSource(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_message_bus(self) -> Callable[ @@ -7220,7 +8140,7 @@ def delete_message_bus(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteMessageBus(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteMessageBus(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_pipeline(self) -> Callable[ @@ -7228,7 +8148,7 @@ def delete_pipeline(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeletePipeline(self._session, self._host, self._interceptor) # type: ignore + return self._DeletePipeline(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_trigger(self) -> Callable[ @@ -7236,7 +8156,7 @@ def delete_trigger(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteTrigger(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteTrigger(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_channel(self) -> Callable[ @@ -7244,7 +8164,7 @@ def get_channel(self) -> Callable[ channel.Channel]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetChannel(self._session, self._host, self._interceptor) # type: ignore + return self._GetChannel(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_channel_connection(self) -> Callable[ @@ -7252,7 +8172,7 @@ def get_channel_connection(self) -> Callable[ channel_connection.ChannelConnection]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetChannelConnection(self._session, self._host, self._interceptor) # type: ignore + return self._GetChannelConnection(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_enrollment(self) -> Callable[ @@ -7260,7 +8180,7 @@ def get_enrollment(self) -> Callable[ enrollment.Enrollment]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetEnrollment(self._session, self._host, self._interceptor) # type: ignore + return self._GetEnrollment(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_google_api_source(self) -> Callable[ @@ -7268,7 +8188,7 @@ def get_google_api_source(self) -> Callable[ google_api_source.GoogleApiSource]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetGoogleApiSource(self._session, self._host, self._interceptor) # type: ignore + return self._GetGoogleApiSource(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_google_channel_config(self) -> Callable[ @@ -7276,7 +8196,7 @@ def get_google_channel_config(self) -> Callable[ google_channel_config.GoogleChannelConfig]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetGoogleChannelConfig(self._session, self._host, self._interceptor) # type: ignore + return self._GetGoogleChannelConfig(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_message_bus(self) -> Callable[ @@ -7284,7 +8204,7 @@ def get_message_bus(self) -> Callable[ message_bus.MessageBus]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetMessageBus(self._session, self._host, self._interceptor) # type: ignore + return self._GetMessageBus(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_pipeline(self) -> Callable[ @@ -7292,7 +8212,7 @@ def get_pipeline(self) -> Callable[ pipeline.Pipeline]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetPipeline(self._session, self._host, self._interceptor) # type: ignore + return self._GetPipeline(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_provider(self) -> Callable[ @@ -7300,7 +8220,7 @@ def get_provider(self) -> Callable[ discovery.Provider]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetProvider(self._session, self._host, self._interceptor) # type: ignore + return self._GetProvider(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_trigger(self) -> Callable[ @@ -7308,7 +8228,7 @@ def get_trigger(self) -> Callable[ trigger.Trigger]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetTrigger(self._session, self._host, self._interceptor) # type: ignore + return self._GetTrigger(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_channel_connections(self) -> Callable[ @@ -7316,7 +8236,7 @@ def list_channel_connections(self) -> Callable[ eventarc.ListChannelConnectionsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListChannelConnections(self._session, self._host, self._interceptor) # type: ignore + return self._ListChannelConnections(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_channels(self) -> Callable[ @@ -7324,7 +8244,7 @@ def list_channels(self) -> Callable[ eventarc.ListChannelsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListChannels(self._session, self._host, self._interceptor) # type: ignore + return self._ListChannels(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_enrollments(self) -> Callable[ @@ -7332,7 +8252,7 @@ def list_enrollments(self) -> Callable[ eventarc.ListEnrollmentsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListEnrollments(self._session, self._host, self._interceptor) # type: ignore + return self._ListEnrollments(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_google_api_sources(self) -> Callable[ @@ -7340,7 +8260,7 @@ def list_google_api_sources(self) -> Callable[ eventarc.ListGoogleApiSourcesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListGoogleApiSources(self._session, self._host, self._interceptor) # type: ignore + return self._ListGoogleApiSources(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_message_bus_enrollments(self) -> Callable[ @@ -7348,7 +8268,7 @@ def list_message_bus_enrollments(self) -> Callable[ eventarc.ListMessageBusEnrollmentsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListMessageBusEnrollments(self._session, self._host, self._interceptor) # type: ignore + return self._ListMessageBusEnrollments(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_message_buses(self) -> Callable[ @@ -7356,7 +8276,7 @@ def list_message_buses(self) -> Callable[ eventarc.ListMessageBusesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListMessageBuses(self._session, self._host, self._interceptor) # type: ignore + return self._ListMessageBuses(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_pipelines(self) -> Callable[ @@ -7364,7 +8284,7 @@ def list_pipelines(self) -> Callable[ eventarc.ListPipelinesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListPipelines(self._session, self._host, self._interceptor) # type: ignore + return self._ListPipelines(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_providers(self) -> Callable[ @@ -7372,7 +8292,7 @@ def list_providers(self) -> Callable[ eventarc.ListProvidersResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListProviders(self._session, self._host, self._interceptor) # type: ignore + return self._ListProviders(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_triggers(self) -> Callable[ @@ -7380,7 +8300,7 @@ def list_triggers(self) -> Callable[ eventarc.ListTriggersResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListTriggers(self._session, self._host, self._interceptor) # type: ignore + return self._ListTriggers(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_channel(self) -> Callable[ @@ -7388,7 +8308,7 @@ def update_channel(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateChannel(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateChannel(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_enrollment(self) -> Callable[ @@ -7396,7 +8316,7 @@ def update_enrollment(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateEnrollment(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateEnrollment(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_google_api_source(self) -> Callable[ @@ -7404,7 +8324,7 @@ def update_google_api_source(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateGoogleApiSource(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateGoogleApiSource(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_google_channel_config(self) -> Callable[ @@ -7412,7 +8332,7 @@ def update_google_channel_config(self) -> Callable[ gce_google_channel_config.GoogleChannelConfig]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateGoogleChannelConfig(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateGoogleChannelConfig(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_message_bus(self) -> Callable[ @@ -7420,7 +8340,7 @@ def update_message_bus(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateMessageBus(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateMessageBus(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_pipeline(self) -> Callable[ @@ -7428,7 +8348,7 @@ def update_pipeline(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdatePipeline(self._session, self._host, self._interceptor) # type: ignore + return self._UpdatePipeline(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_trigger(self) -> Callable[ @@ -7436,11 +8356,11 @@ def update_trigger(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateTrigger(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateTrigger(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseEventarcRestTransport._BaseGetLocation, EventarcRestStub): def __hash__(self): @@ -7454,19 +8374,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -7529,7 +8464,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7563,7 +8507,7 @@ def __call__(self, @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseEventarcRestTransport._BaseListLocations, EventarcRestStub): def __hash__(self): @@ -7577,19 +8521,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -7652,7 +8611,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7686,7 +8654,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseEventarcRestTransport._BaseGetIamPolicy, EventarcRestStub): def __hash__(self): @@ -7700,19 +8668,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -7775,7 +8758,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7809,7 +8801,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseEventarcRestTransport._BaseSetIamPolicy, EventarcRestStub): def __hash__(self): @@ -7823,20 +8815,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -7899,7 +8906,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7933,7 +8950,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseEventarcRestTransport._BaseTestIamPermissions, EventarcRestStub): def __hash__(self): @@ -7947,20 +8964,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -8023,7 +9055,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8057,7 +9099,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseEventarcRestTransport._BaseCancelOperation, EventarcRestStub): def __hash__(self): @@ -8071,20 +9113,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -8144,7 +9201,17 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EventarcRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8155,7 +9222,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseEventarcRestTransport._BaseDeleteOperation, EventarcRestStub): def __hash__(self): @@ -8169,19 +9236,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -8241,7 +9323,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8252,7 +9343,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseEventarcRestTransport._BaseGetOperation, EventarcRestStub): def __hash__(self): @@ -8266,19 +9357,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -8341,7 +9447,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8375,7 +9490,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseEventarcRestTransport._BaseListOperations, EventarcRestStub): def __hash__(self): @@ -8389,19 +9504,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -8464,7 +9594,16 @@ def __call__(self, ) # Send the request - response = EventarcRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EventarcRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest_base.py index 614f97dd14c5..8ee51465cd79 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -61,6 +62,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -81,6 +84,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -96,7 +102,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseCreateChannel: diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py index c73490e1195a..94fe10be590f 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.cloud.eventarc_v1._compat import transcode_request -from google.cloud.eventarc_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.eventarc_v1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.eventarc_v1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.eventarc_v1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 3720a1a84418..937e3558b3c9 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -56,6 +56,7 @@ from google.cloud.eventarc_v1.services.eventarc import EventarcClient from google.cloud.eventarc_v1.services.eventarc import pagers from google.cloud.eventarc_v1.services.eventarc import transports +from google.cloud.eventarc_v1.services.eventarc.client import _observability from google.cloud.eventarc_v1.types import channel from google.cloud.eventarc_v1.types import channel as gce_channel from google.cloud.eventarc_v1.types import channel_connection @@ -781,6 +782,192 @@ def test_eventarc_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_eventarc_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.EventarcGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = EventarcClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_eventarc_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.EventarcGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.EventarcGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_eventarc_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.EventarcGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.EventarcGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_eventarc_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.EventarcGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_eventarc_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.EventarcGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.EventarcGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.EventarcGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.EventarcGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_eventarc_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.EventarcGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.EventarcGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (EventarcClient, transports.EventarcGrpcTransport, "grpc", grpc_helpers), (EventarcAsyncClient, transports.EventarcGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py index 2ec9186dedc1..8ff770a7acf4 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -538,18 +539,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., ConfigServiceV2Transport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py index dada98436600..5928c3dd2280 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version +from google.cloud.logging_v2._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -58,6 +65,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -88,6 +96,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -125,81 +136,145 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_buckets: gapic_v1.method.wrap_method( + self.list_buckets: self._wrap_method( self.list_buckets, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListBuckets", ), - self.get_bucket: gapic_v1.method.wrap_method( + self.get_bucket: self._wrap_method( self.get_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetBucket", ), - self.create_bucket_async: gapic_v1.method.wrap_method( + self.create_bucket_async: self._wrap_method( self.create_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucketAsync", ), - self.update_bucket_async: gapic_v1.method.wrap_method( + self.update_bucket_async: self._wrap_method( self.update_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucketAsync", ), - self.create_bucket: gapic_v1.method.wrap_method( + self.create_bucket: self._wrap_method( self.create_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucket", ), - self.update_bucket: gapic_v1.method.wrap_method( + self.update_bucket: self._wrap_method( self.update_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucket", ), - self.delete_bucket: gapic_v1.method.wrap_method( + self.delete_bucket: self._wrap_method( self.delete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteBucket", ), - self.undelete_bucket: gapic_v1.method.wrap_method( + self.undelete_bucket: self._wrap_method( self.undelete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UndeleteBucket", ), - self.list_views: gapic_v1.method.wrap_method( + self.list_views: self._wrap_method( self.list_views, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListViews", ), - self.get_view: gapic_v1.method.wrap_method( + self.get_view: self._wrap_method( self.get_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetView", ), - self.create_view: gapic_v1.method.wrap_method( + self.create_view: self._wrap_method( self.create_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateView", ), - self.update_view: gapic_v1.method.wrap_method( + self.update_view: self._wrap_method( self.update_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateView", ), - self.delete_view: gapic_v1.method.wrap_method( + self.delete_view: self._wrap_method( self.delete_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteView", ), - self.list_sinks: gapic_v1.method.wrap_method( + self.list_sinks: self._wrap_method( self.list_sinks, default_retry=retries.Retry( initial=0.1, @@ -214,8 +289,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListSinks", ), - self.get_sink: gapic_v1.method.wrap_method( + self.get_sink: self._wrap_method( self.get_sink, default_retry=retries.Retry( initial=0.1, @@ -230,13 +306,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSink", ), - self.create_sink: gapic_v1.method.wrap_method( + self.create_sink: self._wrap_method( self.create_sink, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateSink", ), - self.update_sink: gapic_v1.method.wrap_method( + self.update_sink: self._wrap_method( self.update_sink, default_retry=retries.Retry( initial=0.1, @@ -251,8 +329,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSink", ), - self.delete_sink: gapic_v1.method.wrap_method( + self.delete_sink: self._wrap_method( self.delete_sink, default_retry=retries.Retry( initial=0.1, @@ -267,28 +346,33 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteSink", ), - self.create_link: gapic_v1.method.wrap_method( + self.create_link: self._wrap_method( self.create_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateLink", ), - self.delete_link: gapic_v1.method.wrap_method( + self.delete_link: self._wrap_method( self.delete_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteLink", ), - self.list_links: gapic_v1.method.wrap_method( + self.list_links: self._wrap_method( self.list_links, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListLinks", ), - self.get_link: gapic_v1.method.wrap_method( + self.get_link: self._wrap_method( self.get_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetLink", ), - self.list_exclusions: gapic_v1.method.wrap_method( + self.list_exclusions: self._wrap_method( self.list_exclusions, default_retry=retries.Retry( initial=0.1, @@ -303,8 +387,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListExclusions", ), - self.get_exclusion: gapic_v1.method.wrap_method( + self.get_exclusion: self._wrap_method( self.get_exclusion, default_retry=retries.Retry( initial=0.1, @@ -319,18 +404,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetExclusion", ), - self.create_exclusion: gapic_v1.method.wrap_method( + self.create_exclusion: self._wrap_method( self.create_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateExclusion", ), - self.update_exclusion: gapic_v1.method.wrap_method( + self.update_exclusion: self._wrap_method( self.update_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateExclusion", ), - self.delete_exclusion: gapic_v1.method.wrap_method( + self.delete_exclusion: self._wrap_method( self.delete_exclusion, default_retry=retries.Retry( initial=0.1, @@ -345,46 +433,55 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteExclusion", ), - self.get_cmek_settings: gapic_v1.method.wrap_method( + self.get_cmek_settings: self._wrap_method( self.get_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetCmekSettings", ), - self.update_cmek_settings: gapic_v1.method.wrap_method( + self.update_cmek_settings: self._wrap_method( self.update_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateCmekSettings", ), - self.get_settings: gapic_v1.method.wrap_method( + self.get_settings: self._wrap_method( self.get_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSettings", ), - self.update_settings: gapic_v1.method.wrap_method( + self.update_settings: self._wrap_method( self.update_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSettings", ), - self.copy_log_entries: gapic_v1.method.wrap_method( + self.copy_log_entries: self._wrap_method( self.copy_log_entries, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CopyLogEntries", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py index d8122989787f..543032611b9f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -36,6 +40,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import ConfigServiceV2Transport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -132,6 +140,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -182,6 +192,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -238,6 +254,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -259,8 +276,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index e49afb2aa807..fa63ababe2ae 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -40,6 +43,7 @@ from .base import ConfigServiceV2Transport, DEFAULT_CLIENT_INFO from .grpc import ConfigServiceV2GrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -179,6 +183,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -230,6 +236,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -285,6 +297,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -306,10 +319,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -1273,74 +1299,90 @@ def copy_log_entries(self) -> Callable[ return self._stubs['copy_log_entries'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_buckets: self._wrap_method( + self.list_buckets: self._wrap_async_method( self.list_buckets, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListBuckets", ), - self.get_bucket: self._wrap_method( + self.get_bucket: self._wrap_async_method( self.get_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetBucket", ), - self.create_bucket_async: self._wrap_method( + self.create_bucket_async: self._wrap_async_method( self.create_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucketAsync", ), - self.update_bucket_async: self._wrap_method( + self.update_bucket_async: self._wrap_async_method( self.update_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucketAsync", ), - self.create_bucket: self._wrap_method( + self.create_bucket: self._wrap_async_method( self.create_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucket", ), - self.update_bucket: self._wrap_method( + self.update_bucket: self._wrap_async_method( self.update_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucket", ), - self.delete_bucket: self._wrap_method( + self.delete_bucket: self._wrap_async_method( self.delete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteBucket", ), - self.undelete_bucket: self._wrap_method( + self.undelete_bucket: self._wrap_async_method( self.undelete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UndeleteBucket", ), - self.list_views: self._wrap_method( + self.list_views: self._wrap_async_method( self.list_views, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListViews", ), - self.get_view: self._wrap_method( + self.get_view: self._wrap_async_method( self.get_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetView", ), - self.create_view: self._wrap_method( + self.create_view: self._wrap_async_method( self.create_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateView", ), - self.update_view: self._wrap_method( + self.update_view: self._wrap_async_method( self.update_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateView", ), - self.delete_view: self._wrap_method( + self.delete_view: self._wrap_async_method( self.delete_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteView", ), - self.list_sinks: self._wrap_method( + self.list_sinks: self._wrap_async_method( self.list_sinks, default_retry=retries.AsyncRetry( initial=0.1, @@ -1355,8 +1397,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListSinks", ), - self.get_sink: self._wrap_method( + self.get_sink: self._wrap_async_method( self.get_sink, default_retry=retries.AsyncRetry( initial=0.1, @@ -1371,13 +1414,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSink", ), - self.create_sink: self._wrap_method( + self.create_sink: self._wrap_async_method( self.create_sink, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateSink", ), - self.update_sink: self._wrap_method( + self.update_sink: self._wrap_async_method( self.update_sink, default_retry=retries.AsyncRetry( initial=0.1, @@ -1392,8 +1437,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSink", ), - self.delete_sink: self._wrap_method( + self.delete_sink: self._wrap_async_method( self.delete_sink, default_retry=retries.AsyncRetry( initial=0.1, @@ -1408,28 +1454,33 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteSink", ), - self.create_link: self._wrap_method( + self.create_link: self._wrap_async_method( self.create_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateLink", ), - self.delete_link: self._wrap_method( + self.delete_link: self._wrap_async_method( self.delete_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteLink", ), - self.list_links: self._wrap_method( + self.list_links: self._wrap_async_method( self.list_links, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListLinks", ), - self.get_link: self._wrap_method( + self.get_link: self._wrap_async_method( self.get_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetLink", ), - self.list_exclusions: self._wrap_method( + self.list_exclusions: self._wrap_async_method( self.list_exclusions, default_retry=retries.AsyncRetry( initial=0.1, @@ -1444,8 +1495,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListExclusions", ), - self.get_exclusion: self._wrap_method( + self.get_exclusion: self._wrap_async_method( self.get_exclusion, default_retry=retries.AsyncRetry( initial=0.1, @@ -1460,18 +1512,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetExclusion", ), - self.create_exclusion: self._wrap_method( + self.create_exclusion: self._wrap_async_method( self.create_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateExclusion", ), - self.update_exclusion: self._wrap_method( + self.update_exclusion: self._wrap_async_method( self.update_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateExclusion", ), - self.delete_exclusion: self._wrap_method( + self.delete_exclusion: self._wrap_async_method( self.delete_exclusion, default_retry=retries.AsyncRetry( initial=0.1, @@ -1486,54 +1541,58 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteExclusion", ), - self.get_cmek_settings: self._wrap_method( + self.get_cmek_settings: self._wrap_async_method( self.get_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetCmekSettings", ), - self.update_cmek_settings: self._wrap_method( + self.update_cmek_settings: self._wrap_async_method( self.update_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateCmekSettings", ), - self.get_settings: self._wrap_method( + self.get_settings: self._wrap_async_method( self.get_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSettings", ), - self.update_settings: self._wrap_method( + self.update_settings: self._wrap_async_method( self.update_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSettings", ), - self.copy_log_entries: self._wrap_method( + self.copy_log_entries: self._wrap_async_method( self.copy_log_entries, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CopyLogEntries", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py index dfaf6928a16d..8548da4cb603 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -469,18 +470,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., LoggingServiceV2Transport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 32f2a037688d..2547e10f105b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version +from google.cloud.logging_v2._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -58,6 +65,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -88,6 +96,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -125,16 +136,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.delete_log: gapic_v1.method.wrap_method( + self.delete_log: self._wrap_method( self.delete_log, default_retry=retries.Retry( initial=0.1, @@ -149,8 +211,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/DeleteLog", ), - self.write_log_entries: gapic_v1.method.wrap_method( + self.write_log_entries: self._wrap_method( self.write_log_entries, default_retry=retries.Retry( initial=0.1, @@ -165,8 +228,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/WriteLogEntries", ), - self.list_log_entries: gapic_v1.method.wrap_method( + self.list_log_entries: self._wrap_method( self.list_log_entries, default_retry=retries.Retry( initial=0.1, @@ -181,8 +245,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogEntries", ), - self.list_monitored_resource_descriptors: gapic_v1.method.wrap_method( + self.list_monitored_resource_descriptors: self._wrap_method( self.list_monitored_resource_descriptors, default_retry=retries.Retry( initial=0.1, @@ -197,8 +262,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListMonitoredResourceDescriptors", ), - self.list_logs: gapic_v1.method.wrap_method( + self.list_logs: self._wrap_method( self.list_logs, default_retry=retries.Retry( initial=0.1, @@ -213,8 +279,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogs", ), - self.tail_log_entries: gapic_v1.method.wrap_method( + self.tail_log_entries: self._wrap_method( self.tail_log_entries, default_retry=retries.Retry( initial=0.1, @@ -229,21 +296,26 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=3600.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/TailLogEntries", + is_streaming=True, ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py index eeb3a8564ee0..b4508ffcde56 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -35,6 +39,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import LoggingServiceV2Transport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -131,6 +139,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -181,6 +191,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -236,6 +252,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -257,8 +274,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 8e816f748369..4c2ccce63e6d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -39,6 +42,7 @@ from .base import LoggingServiceV2Transport, DEFAULT_CLIENT_INFO from .grpc import LoggingServiceV2GrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -178,6 +182,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -229,6 +235,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -283,6 +295,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -304,10 +317,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -497,9 +523,12 @@ def tail_log_entries(self) -> Callable[ return self._stubs['tail_log_entries'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.delete_log: self._wrap_method( + self.delete_log: self._wrap_async_method( self.delete_log, default_retry=retries.AsyncRetry( initial=0.1, @@ -514,8 +543,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/DeleteLog", ), - self.write_log_entries: self._wrap_method( + self.write_log_entries: self._wrap_async_method( self.write_log_entries, default_retry=retries.AsyncRetry( initial=0.1, @@ -530,8 +560,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/WriteLogEntries", ), - self.list_log_entries: self._wrap_method( + self.list_log_entries: self._wrap_async_method( self.list_log_entries, default_retry=retries.AsyncRetry( initial=0.1, @@ -546,8 +577,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogEntries", ), - self.list_monitored_resource_descriptors: self._wrap_method( + self.list_monitored_resource_descriptors: self._wrap_async_method( self.list_monitored_resource_descriptors, default_retry=retries.AsyncRetry( initial=0.1, @@ -562,8 +594,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListMonitoredResourceDescriptors", ), - self.list_logs: self._wrap_method( + self.list_logs: self._wrap_async_method( self.list_logs, default_retry=retries.AsyncRetry( initial=0.1, @@ -578,8 +611,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogs", ), - self.tail_log_entries: self._wrap_method( + self.tail_log_entries: self._wrap_async_method( self.tail_log_entries, default_retry=retries.AsyncRetry( initial=0.1, @@ -594,29 +628,29 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=3600.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/TailLogEntries", + is_streaming=True, ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py index 7319be93a38c..fe042f13fee4 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -470,18 +471,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., MetricsServiceV2Transport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index f8a9522a02f5..64f32a0a6022 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version +from google.cloud.logging_v2._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -58,6 +65,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -88,6 +96,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -125,16 +136,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_log_metrics: gapic_v1.method.wrap_method( + self.list_log_metrics: self._wrap_method( self.list_log_metrics, default_retry=retries.Retry( initial=0.1, @@ -149,8 +211,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/ListLogMetrics", ), - self.get_log_metric: gapic_v1.method.wrap_method( + self.get_log_metric: self._wrap_method( self.get_log_metric, default_retry=retries.Retry( initial=0.1, @@ -165,13 +228,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/GetLogMetric", ), - self.create_log_metric: gapic_v1.method.wrap_method( + self.create_log_metric: self._wrap_method( self.create_log_metric, default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/CreateLogMetric", ), - self.update_log_metric: gapic_v1.method.wrap_method( + self.update_log_metric: self._wrap_method( self.update_log_metric, default_retry=retries.Retry( initial=0.1, @@ -186,8 +251,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/UpdateLogMetric", ), - self.delete_log_metric: gapic_v1.method.wrap_method( + self.delete_log_metric: self._wrap_method( self.delete_log_metric, default_retry=retries.Retry( initial=0.1, @@ -202,21 +268,25 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/DeleteLogMetric", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py index 2b6003f77476..f72fc2e40ee4 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -35,6 +39,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import MetricsServiceV2Transport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -131,6 +139,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -181,6 +191,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -236,6 +252,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -257,8 +274,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index aaa422d2953e..00d65df94ad2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -39,6 +42,7 @@ from .base import MetricsServiceV2Transport, DEFAULT_CLIENT_INFO from .grpc import MetricsServiceV2GrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -178,6 +182,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -229,6 +235,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -283,6 +295,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -304,10 +317,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -452,9 +478,12 @@ def delete_log_metric(self) -> Callable[ return self._stubs['delete_log_metric'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_log_metrics: self._wrap_method( + self.list_log_metrics: self._wrap_async_method( self.list_log_metrics, default_retry=retries.AsyncRetry( initial=0.1, @@ -469,8 +498,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/ListLogMetrics", ), - self.get_log_metric: self._wrap_method( + self.get_log_metric: self._wrap_async_method( self.get_log_metric, default_retry=retries.AsyncRetry( initial=0.1, @@ -485,13 +515,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/GetLogMetric", ), - self.create_log_metric: self._wrap_method( + self.create_log_metric: self._wrap_async_method( self.create_log_metric, default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/CreateLogMetric", ), - self.update_log_metric: self._wrap_method( + self.update_log_metric: self._wrap_async_method( self.update_log_metric, default_retry=retries.AsyncRetry( initial=0.1, @@ -506,8 +538,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/UpdateLogMetric", ), - self.delete_log_metric: self._wrap_method( + self.delete_log_metric: self._wrap_async_method( self.delete_log_metric, default_retry=retries.AsyncRetry( initial=0.1, @@ -522,29 +555,28 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/DeleteLogMetric", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py index bce857df2a1d..33c87003c607 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.cloud.logging_v2._compat import transcode_request -from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.logging_v2._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.logging_v2 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.logging_v2 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 638aac7a87f8..6a0372b7a0e7 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -50,6 +50,7 @@ from google.cloud.logging_v2.services.config_service_v2 import ConfigServiceV2Client from google.cloud.logging_v2.services.config_service_v2 import pagers from google.cloud.logging_v2.services.config_service_v2 import transports +from google.cloud.logging_v2.services.config_service_v2.client import _observability from google.cloud.logging_v2.types import logging_config from google.longrunning import operations_pb2 # type: ignore from google.oauth2 import service_account @@ -736,6 +737,192 @@ def test_config_service_v2_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_config_service_v2_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = ConfigServiceV2Client(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_config_service_v2_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.ConfigServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_config_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.ConfigServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_config_service_v2_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.ConfigServiceV2GrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_config_service_v2_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.ConfigServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.ConfigServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.ConfigServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.ConfigServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_config_service_v2_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.ConfigServiceV2GrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.ConfigServiceV2GrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (ConfigServiceV2Client, transports.ConfigServiceV2GrpcTransport, "grpc", grpc_helpers), (ConfigServiceV2AsyncClient, transports.ConfigServiceV2GrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index e1a950c64f4c..20b3b64dd51e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -47,6 +47,7 @@ from google.cloud.logging_v2.services.logging_service_v2 import LoggingServiceV2Client from google.cloud.logging_v2.services.logging_service_v2 import pagers from google.cloud.logging_v2.services.logging_service_v2 import transports +from google.cloud.logging_v2.services.logging_service_v2.client import _observability from google.cloud.logging_v2.types import log_entry from google.cloud.logging_v2.types import logging from google.longrunning import operations_pb2 # type: ignore @@ -737,6 +738,192 @@ def test_logging_service_v2_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_logging_service_v2_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = LoggingServiceV2Client(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_logging_service_v2_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.LoggingServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.LoggingServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_logging_service_v2_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.LoggingServiceV2GrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_logging_service_v2_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.LoggingServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.LoggingServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.LoggingServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.LoggingServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_logging_service_v2_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.LoggingServiceV2GrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.LoggingServiceV2GrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (LoggingServiceV2Client, transports.LoggingServiceV2GrpcTransport, "grpc", grpc_helpers), (LoggingServiceV2AsyncClient, transports.LoggingServiceV2GrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index e2db5c8a9a2a..94dd963c9ac2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -47,6 +47,7 @@ from google.cloud.logging_v2.services.metrics_service_v2 import MetricsServiceV2Client from google.cloud.logging_v2.services.metrics_service_v2 import pagers from google.cloud.logging_v2.services.metrics_service_v2 import transports +from google.cloud.logging_v2.services.metrics_service_v2.client import _observability from google.cloud.logging_v2.types import logging_metrics from google.longrunning import operations_pb2 # type: ignore from google.oauth2 import service_account @@ -735,6 +736,192 @@ def test_metrics_service_v2_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_metrics_service_v2_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = MetricsServiceV2Client(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_metrics_service_v2_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.MetricsServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.MetricsServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_metrics_service_v2_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.MetricsServiceV2GrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_metrics_service_v2_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.MetricsServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.MetricsServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.MetricsServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.MetricsServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_metrics_service_v2_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.MetricsServiceV2GrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.MetricsServiceV2GrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (MetricsServiceV2Client, transports.MetricsServiceV2GrpcTransport, "grpc", grpc_helpers), (MetricsServiceV2AsyncClient, transports.MetricsServiceV2GrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py index e136bf06d85d..e17834023c12 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -538,18 +539,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., ConfigServiceV2Transport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py index dada98436600..5928c3dd2280 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version +from google.cloud.logging_v2._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -58,6 +65,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -88,6 +96,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -125,81 +136,145 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_buckets: gapic_v1.method.wrap_method( + self.list_buckets: self._wrap_method( self.list_buckets, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListBuckets", ), - self.get_bucket: gapic_v1.method.wrap_method( + self.get_bucket: self._wrap_method( self.get_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetBucket", ), - self.create_bucket_async: gapic_v1.method.wrap_method( + self.create_bucket_async: self._wrap_method( self.create_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucketAsync", ), - self.update_bucket_async: gapic_v1.method.wrap_method( + self.update_bucket_async: self._wrap_method( self.update_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucketAsync", ), - self.create_bucket: gapic_v1.method.wrap_method( + self.create_bucket: self._wrap_method( self.create_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucket", ), - self.update_bucket: gapic_v1.method.wrap_method( + self.update_bucket: self._wrap_method( self.update_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucket", ), - self.delete_bucket: gapic_v1.method.wrap_method( + self.delete_bucket: self._wrap_method( self.delete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteBucket", ), - self.undelete_bucket: gapic_v1.method.wrap_method( + self.undelete_bucket: self._wrap_method( self.undelete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UndeleteBucket", ), - self.list_views: gapic_v1.method.wrap_method( + self.list_views: self._wrap_method( self.list_views, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListViews", ), - self.get_view: gapic_v1.method.wrap_method( + self.get_view: self._wrap_method( self.get_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetView", ), - self.create_view: gapic_v1.method.wrap_method( + self.create_view: self._wrap_method( self.create_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateView", ), - self.update_view: gapic_v1.method.wrap_method( + self.update_view: self._wrap_method( self.update_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateView", ), - self.delete_view: gapic_v1.method.wrap_method( + self.delete_view: self._wrap_method( self.delete_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteView", ), - self.list_sinks: gapic_v1.method.wrap_method( + self.list_sinks: self._wrap_method( self.list_sinks, default_retry=retries.Retry( initial=0.1, @@ -214,8 +289,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListSinks", ), - self.get_sink: gapic_v1.method.wrap_method( + self.get_sink: self._wrap_method( self.get_sink, default_retry=retries.Retry( initial=0.1, @@ -230,13 +306,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSink", ), - self.create_sink: gapic_v1.method.wrap_method( + self.create_sink: self._wrap_method( self.create_sink, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateSink", ), - self.update_sink: gapic_v1.method.wrap_method( + self.update_sink: self._wrap_method( self.update_sink, default_retry=retries.Retry( initial=0.1, @@ -251,8 +329,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSink", ), - self.delete_sink: gapic_v1.method.wrap_method( + self.delete_sink: self._wrap_method( self.delete_sink, default_retry=retries.Retry( initial=0.1, @@ -267,28 +346,33 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteSink", ), - self.create_link: gapic_v1.method.wrap_method( + self.create_link: self._wrap_method( self.create_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateLink", ), - self.delete_link: gapic_v1.method.wrap_method( + self.delete_link: self._wrap_method( self.delete_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteLink", ), - self.list_links: gapic_v1.method.wrap_method( + self.list_links: self._wrap_method( self.list_links, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListLinks", ), - self.get_link: gapic_v1.method.wrap_method( + self.get_link: self._wrap_method( self.get_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetLink", ), - self.list_exclusions: gapic_v1.method.wrap_method( + self.list_exclusions: self._wrap_method( self.list_exclusions, default_retry=retries.Retry( initial=0.1, @@ -303,8 +387,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListExclusions", ), - self.get_exclusion: gapic_v1.method.wrap_method( + self.get_exclusion: self._wrap_method( self.get_exclusion, default_retry=retries.Retry( initial=0.1, @@ -319,18 +404,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetExclusion", ), - self.create_exclusion: gapic_v1.method.wrap_method( + self.create_exclusion: self._wrap_method( self.create_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateExclusion", ), - self.update_exclusion: gapic_v1.method.wrap_method( + self.update_exclusion: self._wrap_method( self.update_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateExclusion", ), - self.delete_exclusion: gapic_v1.method.wrap_method( + self.delete_exclusion: self._wrap_method( self.delete_exclusion, default_retry=retries.Retry( initial=0.1, @@ -345,46 +433,55 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteExclusion", ), - self.get_cmek_settings: gapic_v1.method.wrap_method( + self.get_cmek_settings: self._wrap_method( self.get_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetCmekSettings", ), - self.update_cmek_settings: gapic_v1.method.wrap_method( + self.update_cmek_settings: self._wrap_method( self.update_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateCmekSettings", ), - self.get_settings: gapic_v1.method.wrap_method( + self.get_settings: self._wrap_method( self.get_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSettings", ), - self.update_settings: gapic_v1.method.wrap_method( + self.update_settings: self._wrap_method( self.update_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSettings", ), - self.copy_log_entries: gapic_v1.method.wrap_method( + self.copy_log_entries: self._wrap_method( self.copy_log_entries, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CopyLogEntries", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py index d8122989787f..543032611b9f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -36,6 +40,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import ConfigServiceV2Transport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -132,6 +140,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -182,6 +192,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -238,6 +254,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -259,8 +276,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index e49afb2aa807..fa63ababe2ae 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -40,6 +43,7 @@ from .base import ConfigServiceV2Transport, DEFAULT_CLIENT_INFO from .grpc import ConfigServiceV2GrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -179,6 +183,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -230,6 +236,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -285,6 +297,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -306,10 +319,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -1273,74 +1299,90 @@ def copy_log_entries(self) -> Callable[ return self._stubs['copy_log_entries'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_buckets: self._wrap_method( + self.list_buckets: self._wrap_async_method( self.list_buckets, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListBuckets", ), - self.get_bucket: self._wrap_method( + self.get_bucket: self._wrap_async_method( self.get_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetBucket", ), - self.create_bucket_async: self._wrap_method( + self.create_bucket_async: self._wrap_async_method( self.create_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucketAsync", ), - self.update_bucket_async: self._wrap_method( + self.update_bucket_async: self._wrap_async_method( self.update_bucket_async, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucketAsync", ), - self.create_bucket: self._wrap_method( + self.create_bucket: self._wrap_async_method( self.create_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateBucket", ), - self.update_bucket: self._wrap_method( + self.update_bucket: self._wrap_async_method( self.update_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucket", ), - self.delete_bucket: self._wrap_method( + self.delete_bucket: self._wrap_async_method( self.delete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteBucket", ), - self.undelete_bucket: self._wrap_method( + self.undelete_bucket: self._wrap_async_method( self.undelete_bucket, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UndeleteBucket", ), - self.list_views: self._wrap_method( + self.list_views: self._wrap_async_method( self.list_views, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListViews", ), - self.get_view: self._wrap_method( + self.get_view: self._wrap_async_method( self.get_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetView", ), - self.create_view: self._wrap_method( + self.create_view: self._wrap_async_method( self.create_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateView", ), - self.update_view: self._wrap_method( + self.update_view: self._wrap_async_method( self.update_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateView", ), - self.delete_view: self._wrap_method( + self.delete_view: self._wrap_async_method( self.delete_view, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteView", ), - self.list_sinks: self._wrap_method( + self.list_sinks: self._wrap_async_method( self.list_sinks, default_retry=retries.AsyncRetry( initial=0.1, @@ -1355,8 +1397,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListSinks", ), - self.get_sink: self._wrap_method( + self.get_sink: self._wrap_async_method( self.get_sink, default_retry=retries.AsyncRetry( initial=0.1, @@ -1371,13 +1414,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSink", ), - self.create_sink: self._wrap_method( + self.create_sink: self._wrap_async_method( self.create_sink, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateSink", ), - self.update_sink: self._wrap_method( + self.update_sink: self._wrap_async_method( self.update_sink, default_retry=retries.AsyncRetry( initial=0.1, @@ -1392,8 +1437,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSink", ), - self.delete_sink: self._wrap_method( + self.delete_sink: self._wrap_async_method( self.delete_sink, default_retry=retries.AsyncRetry( initial=0.1, @@ -1408,28 +1454,33 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteSink", ), - self.create_link: self._wrap_method( + self.create_link: self._wrap_async_method( self.create_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateLink", ), - self.delete_link: self._wrap_method( + self.delete_link: self._wrap_async_method( self.delete_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteLink", ), - self.list_links: self._wrap_method( + self.list_links: self._wrap_async_method( self.list_links, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListLinks", ), - self.get_link: self._wrap_method( + self.get_link: self._wrap_async_method( self.get_link, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetLink", ), - self.list_exclusions: self._wrap_method( + self.list_exclusions: self._wrap_async_method( self.list_exclusions, default_retry=retries.AsyncRetry( initial=0.1, @@ -1444,8 +1495,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/ListExclusions", ), - self.get_exclusion: self._wrap_method( + self.get_exclusion: self._wrap_async_method( self.get_exclusion, default_retry=retries.AsyncRetry( initial=0.1, @@ -1460,18 +1512,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetExclusion", ), - self.create_exclusion: self._wrap_method( + self.create_exclusion: self._wrap_async_method( self.create_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateExclusion", ), - self.update_exclusion: self._wrap_method( + self.update_exclusion: self._wrap_async_method( self.update_exclusion, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateExclusion", ), - self.delete_exclusion: self._wrap_method( + self.delete_exclusion: self._wrap_async_method( self.delete_exclusion, default_retry=retries.AsyncRetry( initial=0.1, @@ -1486,54 +1541,58 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteExclusion", ), - self.get_cmek_settings: self._wrap_method( + self.get_cmek_settings: self._wrap_async_method( self.get_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetCmekSettings", ), - self.update_cmek_settings: self._wrap_method( + self.update_cmek_settings: self._wrap_async_method( self.update_cmek_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateCmekSettings", ), - self.get_settings: self._wrap_method( + self.get_settings: self._wrap_async_method( self.get_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetSettings", ), - self.update_settings: self._wrap_method( + self.update_settings: self._wrap_async_method( self.update_settings, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateSettings", ), - self.copy_log_entries: self._wrap_method( + self.copy_log_entries: self._wrap_async_method( self.copy_log_entries, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CopyLogEntries", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py index dfaf6928a16d..8548da4cb603 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -469,18 +470,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., LoggingServiceV2Transport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py index 32f2a037688d..2547e10f105b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version +from google.cloud.logging_v2._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -58,6 +65,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -88,6 +96,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -125,16 +136,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.delete_log: gapic_v1.method.wrap_method( + self.delete_log: self._wrap_method( self.delete_log, default_retry=retries.Retry( initial=0.1, @@ -149,8 +211,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/DeleteLog", ), - self.write_log_entries: gapic_v1.method.wrap_method( + self.write_log_entries: self._wrap_method( self.write_log_entries, default_retry=retries.Retry( initial=0.1, @@ -165,8 +228,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/WriteLogEntries", ), - self.list_log_entries: gapic_v1.method.wrap_method( + self.list_log_entries: self._wrap_method( self.list_log_entries, default_retry=retries.Retry( initial=0.1, @@ -181,8 +245,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogEntries", ), - self.list_monitored_resource_descriptors: gapic_v1.method.wrap_method( + self.list_monitored_resource_descriptors: self._wrap_method( self.list_monitored_resource_descriptors, default_retry=retries.Retry( initial=0.1, @@ -197,8 +262,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListMonitoredResourceDescriptors", ), - self.list_logs: gapic_v1.method.wrap_method( + self.list_logs: self._wrap_method( self.list_logs, default_retry=retries.Retry( initial=0.1, @@ -213,8 +279,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogs", ), - self.tail_log_entries: gapic_v1.method.wrap_method( + self.tail_log_entries: self._wrap_method( self.tail_log_entries, default_retry=retries.Retry( initial=0.1, @@ -229,21 +296,26 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=3600.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/TailLogEntries", + is_streaming=True, ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py index eeb3a8564ee0..b4508ffcde56 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -35,6 +39,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import LoggingServiceV2Transport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -131,6 +139,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -181,6 +191,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -236,6 +252,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -257,8 +274,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 8e816f748369..4c2ccce63e6d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -39,6 +42,7 @@ from .base import LoggingServiceV2Transport, DEFAULT_CLIENT_INFO from .grpc import LoggingServiceV2GrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -178,6 +182,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -229,6 +235,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -283,6 +295,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -304,10 +317,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -497,9 +523,12 @@ def tail_log_entries(self) -> Callable[ return self._stubs['tail_log_entries'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.delete_log: self._wrap_method( + self.delete_log: self._wrap_async_method( self.delete_log, default_retry=retries.AsyncRetry( initial=0.1, @@ -514,8 +543,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/DeleteLog", ), - self.write_log_entries: self._wrap_method( + self.write_log_entries: self._wrap_async_method( self.write_log_entries, default_retry=retries.AsyncRetry( initial=0.1, @@ -530,8 +560,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/WriteLogEntries", ), - self.list_log_entries: self._wrap_method( + self.list_log_entries: self._wrap_async_method( self.list_log_entries, default_retry=retries.AsyncRetry( initial=0.1, @@ -546,8 +577,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogEntries", ), - self.list_monitored_resource_descriptors: self._wrap_method( + self.list_monitored_resource_descriptors: self._wrap_async_method( self.list_monitored_resource_descriptors, default_retry=retries.AsyncRetry( initial=0.1, @@ -562,8 +594,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListMonitoredResourceDescriptors", ), - self.list_logs: self._wrap_method( + self.list_logs: self._wrap_async_method( self.list_logs, default_retry=retries.AsyncRetry( initial=0.1, @@ -578,8 +611,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/ListLogs", ), - self.tail_log_entries: self._wrap_method( + self.tail_log_entries: self._wrap_async_method( self.tail_log_entries, default_retry=retries.AsyncRetry( initial=0.1, @@ -594,29 +628,29 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=3600.0, client_info=client_info, + method_name="google.logging.v2.LoggingServiceV2/TailLogEntries", + is_streaming=True, ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py index 46949c293cd9..9cb5795838c9 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -470,18 +471,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., MetricsServiceV2Transport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py index f8a9522a02f5..64f32a0a6022 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version +from google.cloud.logging_v2._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -58,6 +65,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -88,6 +96,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -125,16 +136,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_log_metrics: gapic_v1.method.wrap_method( + self.list_log_metrics: self._wrap_method( self.list_log_metrics, default_retry=retries.Retry( initial=0.1, @@ -149,8 +211,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/ListLogMetrics", ), - self.get_log_metric: gapic_v1.method.wrap_method( + self.get_log_metric: self._wrap_method( self.get_log_metric, default_retry=retries.Retry( initial=0.1, @@ -165,13 +228,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/GetLogMetric", ), - self.create_log_metric: gapic_v1.method.wrap_method( + self.create_log_metric: self._wrap_method( self.create_log_metric, default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/CreateLogMetric", ), - self.update_log_metric: gapic_v1.method.wrap_method( + self.update_log_metric: self._wrap_method( self.update_log_metric, default_retry=retries.Retry( initial=0.1, @@ -186,8 +251,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/UpdateLogMetric", ), - self.delete_log_metric: gapic_v1.method.wrap_method( + self.delete_log_metric: self._wrap_method( self.delete_log_metric, default_retry=retries.Retry( initial=0.1, @@ -202,21 +268,25 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/DeleteLogMetric", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py index 2b6003f77476..f72fc2e40ee4 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -35,6 +39,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import MetricsServiceV2Transport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -131,6 +139,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -181,6 +191,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -236,6 +252,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -257,8 +274,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index aaa422d2953e..00d65df94ad2 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.logging_v2._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -39,6 +42,7 @@ from .base import MetricsServiceV2Transport, DEFAULT_CLIENT_INFO from .grpc import MetricsServiceV2GrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -178,6 +182,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -229,6 +235,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -283,6 +295,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -304,10 +317,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -452,9 +478,12 @@ def delete_log_metric(self) -> Callable[ return self._stubs['delete_log_metric'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_log_metrics: self._wrap_method( + self.list_log_metrics: self._wrap_async_method( self.list_log_metrics, default_retry=retries.AsyncRetry( initial=0.1, @@ -469,8 +498,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/ListLogMetrics", ), - self.get_log_metric: self._wrap_method( + self.get_log_metric: self._wrap_async_method( self.get_log_metric, default_retry=retries.AsyncRetry( initial=0.1, @@ -485,13 +515,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/GetLogMetric", ), - self.create_log_metric: self._wrap_method( + self.create_log_metric: self._wrap_async_method( self.create_log_metric, default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/CreateLogMetric", ), - self.update_log_metric: self._wrap_method( + self.update_log_metric: self._wrap_async_method( self.update_log_metric, default_retry=retries.AsyncRetry( initial=0.1, @@ -506,8 +538,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/UpdateLogMetric", ), - self.delete_log_metric: self._wrap_method( + self.delete_log_metric: self._wrap_async_method( self.delete_log_metric, default_retry=retries.AsyncRetry( initial=0.1, @@ -522,29 +555,28 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.logging.v2.MetricsServiceV2/DeleteLogMetric", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py index bce857df2a1d..33c87003c607 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.cloud.logging_v2._compat import transcode_request -from google.cloud.logging_v2._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.logging_v2._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.logging_v2 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.logging_v2 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index c63237e51f6c..c958e82e22f1 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -50,6 +50,7 @@ from google.cloud.logging_v2.services.config_service_v2 import BaseConfigServiceV2Client from google.cloud.logging_v2.services.config_service_v2 import pagers from google.cloud.logging_v2.services.config_service_v2 import transports +from google.cloud.logging_v2.services.config_service_v2.client import _observability from google.cloud.logging_v2.types import logging_config from google.longrunning import operations_pb2 # type: ignore from google.oauth2 import service_account @@ -736,6 +737,192 @@ def test_base_config_service_v2_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_base_config_service_v2_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = BaseConfigServiceV2Client(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_config_service_v2_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.ConfigServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_config_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.ConfigServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_config_service_v2_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.ConfigServiceV2GrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_config_service_v2_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.ConfigServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.ConfigServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.ConfigServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.ConfigServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_config_service_v2_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.ConfigServiceV2GrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.ConfigServiceV2GrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (BaseConfigServiceV2Client, transports.ConfigServiceV2GrpcTransport, "grpc", grpc_helpers), (BaseConfigServiceV2AsyncClient, transports.ConfigServiceV2GrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index e1a950c64f4c..20b3b64dd51e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -47,6 +47,7 @@ from google.cloud.logging_v2.services.logging_service_v2 import LoggingServiceV2Client from google.cloud.logging_v2.services.logging_service_v2 import pagers from google.cloud.logging_v2.services.logging_service_v2 import transports +from google.cloud.logging_v2.services.logging_service_v2.client import _observability from google.cloud.logging_v2.types import log_entry from google.cloud.logging_v2.types import logging from google.longrunning import operations_pb2 # type: ignore @@ -737,6 +738,192 @@ def test_logging_service_v2_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_logging_service_v2_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = LoggingServiceV2Client(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_logging_service_v2_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.LoggingServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.LoggingServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_logging_service_v2_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.LoggingServiceV2GrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_logging_service_v2_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.LoggingServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.LoggingServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.LoggingServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.LoggingServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_logging_service_v2_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.LoggingServiceV2GrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.LoggingServiceV2GrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (LoggingServiceV2Client, transports.LoggingServiceV2GrpcTransport, "grpc", grpc_helpers), (LoggingServiceV2AsyncClient, transports.LoggingServiceV2GrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 5cb0ed20e2b1..b613eed4163a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -47,6 +47,7 @@ from google.cloud.logging_v2.services.metrics_service_v2 import BaseMetricsServiceV2Client from google.cloud.logging_v2.services.metrics_service_v2 import pagers from google.cloud.logging_v2.services.metrics_service_v2 import transports +from google.cloud.logging_v2.services.metrics_service_v2.client import _observability from google.cloud.logging_v2.types import logging_metrics from google.longrunning import operations_pb2 # type: ignore from google.oauth2 import service_account @@ -735,6 +736,192 @@ def test_base_metrics_service_v2_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_base_metrics_service_v2_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = BaseMetricsServiceV2Client(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_metrics_service_v2_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.MetricsServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.MetricsServiceV2GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_metrics_service_v2_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.MetricsServiceV2GrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_metrics_service_v2_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.MetricsServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.MetricsServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.MetricsServiceV2GrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.MetricsServiceV2GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_metrics_service_v2_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.MetricsServiceV2GrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.MetricsServiceV2GrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (BaseMetricsServiceV2Client, transports.MetricsServiceV2GrpcTransport, "grpc", grpc_helpers), (BaseMetricsServiceV2AsyncClient, transports.MetricsServiceV2GrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index bb62ed511a5c..6db0da86a58d 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.redis_v1._compat import _observability from google.cloud.redis_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -520,10 +521,14 @@ def __init__(self, *, raise core_exceptions.AsyncRestUnsupportedParameterError( # type: ignore f"The following provided parameters are not supported for `transport=rest_asyncio`: {', '.join(provided_unsupported_params)}" ) + client_options = None + if _observability is not None and _observability.is_otel_capabilities_enabled(self._client_options): # pragma: NO COVER + client_options = self._client_options # pragma: NO COVER self._transport = transport_init( credentials=credentials, host=self._api_endpoint, client_info=client_info, + **({"client_options": client_options} if client_options is not None else {}), ) return @@ -532,18 +537,32 @@ def __init__(self, *, if api_key_value and hasattr(google.auth._default, "get_api_key_credentials"): credentials = google.auth._default.get_api_key_credentials(api_key_value) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 8e015f903a92..a4f11b9bf2b0 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version +from google.cloud.redis_v1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -55,6 +62,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -85,6 +93,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -122,104 +133,173 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_instances: gapic_v1.method.wrap_method( + self.list_instances: self._wrap_method( self.list_instances, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ListInstances", ), - self.get_instance: gapic_v1.method.wrap_method( + self.get_instance: self._wrap_method( self.get_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstance", ), - self.get_instance_auth_string: gapic_v1.method.wrap_method( + self.get_instance_auth_string: self._wrap_method( self.get_instance_auth_string, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstanceAuthString", ), - self.create_instance: gapic_v1.method.wrap_method( + self.create_instance: self._wrap_method( self.create_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/CreateInstance", ), - self.update_instance: gapic_v1.method.wrap_method( + self.update_instance: self._wrap_method( self.update_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpdateInstance", ), - self.upgrade_instance: gapic_v1.method.wrap_method( + self.upgrade_instance: self._wrap_method( self.upgrade_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpgradeInstance", ), - self.import_instance: gapic_v1.method.wrap_method( + self.import_instance: self._wrap_method( self.import_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ImportInstance", ), - self.export_instance: gapic_v1.method.wrap_method( + self.export_instance: self._wrap_method( self.export_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ExportInstance", ), - self.failover_instance: gapic_v1.method.wrap_method( + self.failover_instance: self._wrap_method( self.failover_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/FailoverInstance", ), - self.delete_instance: gapic_v1.method.wrap_method( + self.delete_instance: self._wrap_method( self.delete_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/DeleteInstance", ), - self.reschedule_maintenance: gapic_v1.method.wrap_method( + self.reschedule_maintenance: self._wrap_method( self.reschedule_maintenance, default_timeout=None, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/RescheduleMaintenance", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.wait_operation: gapic_v1.method.wrap_method( + self.wait_operation: self._wrap_method( self.wait_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/WaitOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py index addfbf37e166..1a36444c1eb2 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.redis_v1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -36,6 +40,10 @@ from google.longrunning import operations_pb2 # type: ignore from .base import CloudRedisTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -152,6 +160,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -202,6 +212,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -258,6 +274,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -279,8 +296,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index 110d71537636..f9045ac7e06b 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.redis_v1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -40,6 +43,7 @@ from .base import CloudRedisTransport, DEFAULT_CLIENT_INFO from .grpc import CloudRedisGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -199,6 +203,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -250,6 +256,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -305,6 +317,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -326,10 +339,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -694,105 +720,121 @@ def reschedule_maintenance(self) -> Callable[ return self._stubs['reschedule_maintenance'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_instances: self._wrap_method( + self.list_instances: self._wrap_async_method( self.list_instances, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ListInstances", ), - self.get_instance: self._wrap_method( + self.get_instance: self._wrap_async_method( self.get_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstance", ), - self.get_instance_auth_string: self._wrap_method( + self.get_instance_auth_string: self._wrap_async_method( self.get_instance_auth_string, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstanceAuthString", ), - self.create_instance: self._wrap_method( + self.create_instance: self._wrap_async_method( self.create_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/CreateInstance", ), - self.update_instance: self._wrap_method( + self.update_instance: self._wrap_async_method( self.update_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpdateInstance", ), - self.upgrade_instance: self._wrap_method( + self.upgrade_instance: self._wrap_async_method( self.upgrade_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpgradeInstance", ), - self.import_instance: self._wrap_method( + self.import_instance: self._wrap_async_method( self.import_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ImportInstance", ), - self.export_instance: self._wrap_method( + self.export_instance: self._wrap_async_method( self.export_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ExportInstance", ), - self.failover_instance: self._wrap_method( + self.failover_instance: self._wrap_async_method( self.failover_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/FailoverInstance", ), - self.delete_instance: self._wrap_method( + self.delete_instance: self._wrap_async_method( self.delete_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/DeleteInstance", ), - self.reschedule_maintenance: self._wrap_method( + self.reschedule_maintenance: self._wrap_async_method( self.reschedule_maintenance, default_timeout=None, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/RescheduleMaintenance", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.wait_operation: self._wrap_method( + self.wait_operation: self._wrap_async_method( self.wait_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/WaitOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py index ea8778e47a84..282ad08fec80 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.cloud.redis_v1._compat import transcode_request +from google.cloud.redis_v1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -722,6 +723,7 @@ class CloudRedisRestStub: _session: AuthorizedSession _host: str _interceptor: CloudRedisRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class CloudRedisRestTransport(_BaseCloudRedisRestTransport): @@ -769,6 +771,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[CloudRedisRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -812,6 +816,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -823,7 +830,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -901,20 +910,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -981,7 +1005,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._CreateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._CreateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -991,7 +1025,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_instance_with_metadata(resp, response_metadata) @@ -1028,19 +1061,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -1107,7 +1155,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._DeleteInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._DeleteInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1117,7 +1174,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_instance_with_metadata(resp, response_metadata) @@ -1154,20 +1210,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.ExportInstanceRequest, *, @@ -1234,7 +1305,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ExportInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._ExportInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1244,7 +1325,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_export_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_export_instance_with_metadata(resp, response_metadata) @@ -1281,20 +1361,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.FailoverInstanceRequest, *, @@ -1361,7 +1456,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._FailoverInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._FailoverInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1371,7 +1476,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_failover_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_failover_instance_with_metadata(resp, response_metadata) @@ -1408,19 +1512,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -1484,7 +1603,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1496,7 +1624,6 @@ def __call__(self, pb_resp = cloud_redis.Instance.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_instance_with_metadata(resp, response_metadata) @@ -1533,19 +1660,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.GetInstanceAuthStringRequest, *, @@ -1609,7 +1751,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetInstanceAuthString._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetInstanceAuthString._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1621,7 +1772,6 @@ def __call__(self, pb_resp = cloud_redis.InstanceAuthString.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_instance_auth_string(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_instance_auth_string_with_metadata(resp, response_metadata) @@ -1658,20 +1808,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.ImportInstanceRequest, *, @@ -1738,7 +1903,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ImportInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._ImportInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1748,7 +1923,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_import_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_import_instance_with_metadata(resp, response_metadata) @@ -1785,19 +1959,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1863,7 +2052,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ListInstances._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._ListInstances._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1875,7 +2073,6 @@ def __call__(self, pb_resp = cloud_redis.ListInstancesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_instances(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_instances_with_metadata(resp, response_metadata) @@ -1912,20 +2109,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.RescheduleMaintenanceRequest, *, @@ -1992,7 +2204,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._RescheduleMaintenance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._RescheduleMaintenance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2002,7 +2224,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_reschedule_maintenance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_reschedule_maintenance_with_metadata(resp, response_metadata) @@ -2039,20 +2260,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -2119,7 +2355,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._UpdateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._UpdateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2129,7 +2375,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_instance_with_metadata(resp, response_metadata) @@ -2166,20 +2411,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.UpgradeInstanceRequest, *, @@ -2246,7 +2506,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._UpgradeInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._UpgradeInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2256,7 +2526,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_upgrade_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_upgrade_instance_with_metadata(resp, response_metadata) @@ -2287,7 +2556,7 @@ def create_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._CreateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_instance(self) -> Callable[ @@ -2295,7 +2564,7 @@ def delete_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def export_instance(self) -> Callable[ @@ -2303,7 +2572,7 @@ def export_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ExportInstance(self._session, self._host, self._interceptor) # type: ignore + return self._ExportInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def failover_instance(self) -> Callable[ @@ -2311,7 +2580,7 @@ def failover_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._FailoverInstance(self._session, self._host, self._interceptor) # type: ignore + return self._FailoverInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_instance(self) -> Callable[ @@ -2319,7 +2588,7 @@ def get_instance(self) -> Callable[ cloud_redis.Instance]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore + return self._GetInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_instance_auth_string(self) -> Callable[ @@ -2327,7 +2596,7 @@ def get_instance_auth_string(self) -> Callable[ cloud_redis.InstanceAuthString]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetInstanceAuthString(self._session, self._host, self._interceptor) # type: ignore + return self._GetInstanceAuthString(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def import_instance(self) -> Callable[ @@ -2335,7 +2604,7 @@ def import_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ImportInstance(self._session, self._host, self._interceptor) # type: ignore + return self._ImportInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_instances(self) -> Callable[ @@ -2343,7 +2612,7 @@ def list_instances(self) -> Callable[ cloud_redis.ListInstancesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore + return self._ListInstances(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def reschedule_maintenance(self) -> Callable[ @@ -2351,7 +2620,7 @@ def reschedule_maintenance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._RescheduleMaintenance(self._session, self._host, self._interceptor) # type: ignore + return self._RescheduleMaintenance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_instance(self) -> Callable[ @@ -2359,7 +2628,7 @@ def update_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def upgrade_instance(self) -> Callable[ @@ -2367,11 +2636,11 @@ def upgrade_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpgradeInstance(self._session, self._host, self._interceptor) # type: ignore + return self._UpgradeInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseCloudRedisRestTransport._BaseGetLocation, CloudRedisRestStub): def __hash__(self): @@ -2385,19 +2654,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -2460,7 +2744,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2494,7 +2787,7 @@ def __call__(self, @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseCloudRedisRestTransport._BaseListLocations, CloudRedisRestStub): def __hash__(self): @@ -2508,19 +2801,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -2583,7 +2891,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2617,7 +2934,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseCloudRedisRestTransport._BaseCancelOperation, CloudRedisRestStub): def __hash__(self): @@ -2631,19 +2948,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2703,7 +3035,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2714,7 +3055,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseCloudRedisRestTransport._BaseDeleteOperation, CloudRedisRestStub): def __hash__(self): @@ -2728,19 +3069,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2800,7 +3156,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2811,7 +3176,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseCloudRedisRestTransport._BaseGetOperation, CloudRedisRestStub): def __hash__(self): @@ -2825,19 +3190,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2900,7 +3280,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2934,7 +3323,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseCloudRedisRestTransport._BaseListOperations, CloudRedisRestStub): def __hash__(self): @@ -2948,19 +3337,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -3023,7 +3427,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3057,7 +3470,7 @@ def __call__(self, @property def wait_operation(self): - return self._WaitOperation(self._session, self._host, self._interceptor) # type: ignore + return self._WaitOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _WaitOperation(_BaseCloudRedisRestTransport._BaseWaitOperation, CloudRedisRestStub): def __hash__(self): @@ -3071,20 +3484,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -3147,7 +3575,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._WaitOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._WaitOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index fa3f65ce9e36..564920114b7d 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -28,12 +28,13 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore from google.api_core import retry_async as retries from google.api_core import rest_helpers from google.api_core import rest_streaming_async # type: ignore -from google.cloud.redis_v1._compat import transcode_request +from google.cloud.redis_v1._compat import transcode_request, trace_http_request import google.protobuf @@ -736,6 +737,7 @@ class AsyncCloudRedisRestStub: _session: AsyncAuthorizedSession _host: str _interceptor: AsyncCloudRedisRestInterceptor + _client_options: Optional[ClientOptions] = None class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): """Asynchronous REST backend transport for CloudRedis. @@ -775,6 +777,8 @@ def __init__(self, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[AsyncCloudRedisRestInterceptor] = None, + client_options: Optional[ClientOptions] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -800,6 +804,9 @@ def __init__(self, "http" can be specified. interceptor (Optional[AsyncCloudRedisRestInterceptor]): Interceptor used to manipulate requests, request metadata, and responses. + client_options (Optional[google.api_core.client_options.ClientOptions]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor super().__init__( @@ -808,114 +815,131 @@ def __init__(self, client_info=client_info, always_use_jwt_access=False, url_scheme=url_scheme, - api_audience=None + api_audience=None, + client_options=client_options, + **kwargs, ) self._session = AsyncAuthorizedSession(self._credentials) # type: ignore self._interceptor = interceptor or AsyncCloudRedisRestInterceptor() - self._wrap_with_kind = True self._prep_wrapped_messages(client_info) self._operations_client: Optional[operations_v1.AsyncOperationsRestClient] = None def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_instances: self._wrap_method( + self.list_instances: self._wrap_async_method( self.list_instances, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ListInstances", ), - self.get_instance: self._wrap_method( + self.get_instance: self._wrap_async_method( self.get_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstance", ), - self.get_instance_auth_string: self._wrap_method( + self.get_instance_auth_string: self._wrap_async_method( self.get_instance_auth_string, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstanceAuthString", ), - self.create_instance: self._wrap_method( + self.create_instance: self._wrap_async_method( self.create_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/CreateInstance", ), - self.update_instance: self._wrap_method( + self.update_instance: self._wrap_async_method( self.update_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpdateInstance", ), - self.upgrade_instance: self._wrap_method( + self.upgrade_instance: self._wrap_async_method( self.upgrade_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpgradeInstance", ), - self.import_instance: self._wrap_method( + self.import_instance: self._wrap_async_method( self.import_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ImportInstance", ), - self.export_instance: self._wrap_method( + self.export_instance: self._wrap_async_method( self.export_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ExportInstance", ), - self.failover_instance: self._wrap_method( + self.failover_instance: self._wrap_async_method( self.failover_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/FailoverInstance", ), - self.delete_instance: self._wrap_method( + self.delete_instance: self._wrap_async_method( self.delete_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/DeleteInstance", ), - self.reschedule_maintenance: self._wrap_method( + self.reschedule_maintenance: self._wrap_async_method( self.reschedule_maintenance, default_timeout=None, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/RescheduleMaintenance", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.wait_operation: self._wrap_method( + self.wait_operation: self._wrap_async_method( self.wait_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/WaitOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - class _CreateInstance(_BaseCloudRedisRestTransport._BaseCreateInstance, AsyncCloudRedisRestStub): def __hash__(self): return hash("AsyncCloudRedisRestTransport.CreateInstance") @@ -928,20 +952,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -1008,7 +1047,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._CreateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._CreateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1061,19 +1110,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -1140,7 +1204,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._DeleteInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._DeleteInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1193,20 +1266,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.ExportInstanceRequest, *, @@ -1273,7 +1361,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ExportInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._ExportInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1326,20 +1424,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.FailoverInstanceRequest, *, @@ -1406,7 +1519,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._FailoverInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._FailoverInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1459,19 +1582,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -1535,7 +1673,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1588,19 +1735,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.GetInstanceAuthStringRequest, *, @@ -1664,7 +1826,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetInstanceAuthString._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetInstanceAuthString._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1717,20 +1888,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.ImportInstanceRequest, *, @@ -1797,7 +1983,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ImportInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._ImportInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1850,19 +2046,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1928,7 +2139,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ListInstances._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._ListInstances._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1981,20 +2201,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.RescheduleMaintenanceRequest, *, @@ -2061,7 +2296,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._RescheduleMaintenance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._RescheduleMaintenance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2114,20 +2359,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -2194,7 +2454,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._UpdateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._UpdateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2247,20 +2517,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.UpgradeInstanceRequest, *, @@ -2327,7 +2612,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._UpgradeInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._UpgradeInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2428,71 +2723,71 @@ def operations_client(self) -> AsyncOperationsRestClient: def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], operations_pb2.Operation]: - return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._CreateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], operations_pb2.Operation]: - return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def export_instance(self) -> Callable[ [cloud_redis.ExportInstanceRequest], operations_pb2.Operation]: - return self._ExportInstance(self._session, self._host, self._interceptor) # type: ignore + return self._ExportInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def failover_instance(self) -> Callable[ [cloud_redis.FailoverInstanceRequest], operations_pb2.Operation]: - return self._FailoverInstance(self._session, self._host, self._interceptor) # type: ignore + return self._FailoverInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], cloud_redis.Instance]: - return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore + return self._GetInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_instance_auth_string(self) -> Callable[ [cloud_redis.GetInstanceAuthStringRequest], cloud_redis.InstanceAuthString]: - return self._GetInstanceAuthString(self._session, self._host, self._interceptor) # type: ignore + return self._GetInstanceAuthString(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def import_instance(self) -> Callable[ [cloud_redis.ImportInstanceRequest], operations_pb2.Operation]: - return self._ImportInstance(self._session, self._host, self._interceptor) # type: ignore + return self._ImportInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], cloud_redis.ListInstancesResponse]: - return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore + return self._ListInstances(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def reschedule_maintenance(self) -> Callable[ [cloud_redis.RescheduleMaintenanceRequest], operations_pb2.Operation]: - return self._RescheduleMaintenance(self._session, self._host, self._interceptor) # type: ignore + return self._RescheduleMaintenance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], operations_pb2.Operation]: - return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def upgrade_instance(self) -> Callable[ [cloud_redis.UpgradeInstanceRequest], operations_pb2.Operation]: - return self._UpgradeInstance(self._session, self._host, self._interceptor) # type: ignore + return self._UpgradeInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseCloudRedisRestTransport._BaseGetLocation, AsyncCloudRedisRestStub): def __hash__(self): @@ -2506,19 +2801,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -2581,7 +2891,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2619,7 +2938,7 @@ async def __call__(self, @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseCloudRedisRestTransport._BaseListLocations, AsyncCloudRedisRestStub): def __hash__(self): @@ -2633,19 +2952,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -2708,7 +3042,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2746,7 +3089,7 @@ async def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseCloudRedisRestTransport._BaseCancelOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -2760,19 +3103,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2832,7 +3190,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2847,7 +3214,7 @@ async def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseCloudRedisRestTransport._BaseDeleteOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -2861,19 +3228,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2933,7 +3315,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2948,7 +3339,7 @@ async def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseCloudRedisRestTransport._BaseGetOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -2962,19 +3353,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -3037,7 +3443,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3075,7 +3490,7 @@ async def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseCloudRedisRestTransport._BaseListOperations, AsyncCloudRedisRestStub): def __hash__(self): @@ -3089,19 +3504,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -3164,7 +3594,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3202,7 +3641,7 @@ async def __call__(self, @property def wait_operation(self): - return self._WaitOperation(self._session, self._host, self._interceptor) # type: ignore + return self._WaitOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _WaitOperation(_BaseCloudRedisRestTransport._BaseWaitOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -3216,20 +3655,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -3292,7 +3746,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._WaitOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._WaitOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py index 972b14a1295e..eca2baf79f05 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.cloud.location import locations_pb2 # type: ignore @@ -49,6 +50,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -69,6 +72,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -84,7 +90,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseCreateInstance: diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 6bd8b8b5009c..262e2c3f072b 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -64,6 +64,7 @@ from google.cloud.redis_v1.services.cloud_redis import CloudRedisClient from google.cloud.redis_v1.services.cloud_redis import pagers from google.cloud.redis_v1.services.cloud_redis import transports +from google.cloud.redis_v1.services.cloud_redis.client import _observability from google.cloud.redis_v1.types import cloud_redis from google.longrunning import operations_pb2 # type: ignore from google.oauth2 import service_account @@ -768,6 +769,192 @@ def test_cloud_redis_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_cloud_redis_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.CloudRedisGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = CloudRedisClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_cloud_redis_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.CloudRedisGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.CloudRedisGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_cloud_redis_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.CloudRedisGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.CloudRedisGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_cloud_redis_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.CloudRedisGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_cloud_redis_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.CloudRedisGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.CloudRedisGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.CloudRedisGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.CloudRedisGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_cloud_redis_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.CloudRedisGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.CloudRedisGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (CloudRedisClient, transports.CloudRedisGrpcTransport, "grpc", grpc_helpers), (CloudRedisAsyncClient, transports.CloudRedisGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py index f7fe8e355981..da6c3dbaaad3 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.cloud.redis_v1._compat import transcode_request -from google.cloud.redis_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.redis_v1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.redis_v1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.redis_v1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py index a6d3f9fbb31f..5946aadc8f20 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/_compat.py @@ -18,14 +18,172 @@ import os import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py index 22043152a1c6..17f82b11a062 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py @@ -27,6 +27,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.redis_v1._compat import _observability from google.cloud.redis_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -520,10 +521,14 @@ def __init__(self, *, raise core_exceptions.AsyncRestUnsupportedParameterError( # type: ignore f"The following provided parameters are not supported for `transport=rest_asyncio`: {', '.join(provided_unsupported_params)}" ) + client_options = None + if _observability is not None and _observability.is_otel_capabilities_enabled(self._client_options): # pragma: NO COVER + client_options = self._client_options # pragma: NO COVER self._transport = transport_init( credentials=credentials, host=self._api_endpoint, client_info=client_info, + **({"client_options": client_options} if client_options is not None else {}), ) return @@ -532,18 +537,32 @@ def __init__(self, *, if api_key_value and hasattr(google.auth._default, "get_api_key_credentials"): credentials = google.auth._default.get_api_key_credentials(api_key_value) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py index 8b9a24ec87fa..327984cec569 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version +from google.cloud.redis_v1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -55,6 +62,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -85,6 +93,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -122,74 +133,137 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_instances: gapic_v1.method.wrap_method( + self.list_instances: self._wrap_method( self.list_instances, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ListInstances", ), - self.get_instance: gapic_v1.method.wrap_method( + self.get_instance: self._wrap_method( self.get_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstance", ), - self.create_instance: gapic_v1.method.wrap_method( + self.create_instance: self._wrap_method( self.create_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/CreateInstance", ), - self.update_instance: gapic_v1.method.wrap_method( + self.update_instance: self._wrap_method( self.update_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpdateInstance", ), - self.delete_instance: gapic_v1.method.wrap_method( + self.delete_instance: self._wrap_method( self.delete_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/DeleteInstance", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.wait_operation: gapic_v1.method.wrap_method( + self.wait_operation: self._wrap_method( self.wait_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/WaitOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py index cae682b3d0ae..287bcfc447cb 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.redis_v1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -36,6 +40,10 @@ from google.longrunning import operations_pb2 # type: ignore from .base import CloudRedisTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -152,6 +160,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -202,6 +212,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -258,6 +274,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -279,8 +296,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index c7b03489475f..db914f4b2471 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.redis_v1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -40,6 +43,7 @@ from .base import CloudRedisTransport, DEFAULT_CLIENT_INFO from .grpc import CloudRedisGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -199,6 +203,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -250,6 +256,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -305,6 +317,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -326,10 +339,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -518,75 +544,85 @@ def delete_instance(self) -> Callable[ return self._stubs['delete_instance'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_instances: self._wrap_method( + self.list_instances: self._wrap_async_method( self.list_instances, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ListInstances", ), - self.get_instance: self._wrap_method( + self.get_instance: self._wrap_async_method( self.get_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstance", ), - self.create_instance: self._wrap_method( + self.create_instance: self._wrap_async_method( self.create_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/CreateInstance", ), - self.update_instance: self._wrap_method( + self.update_instance: self._wrap_async_method( self.update_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpdateInstance", ), - self.delete_instance: self._wrap_method( + self.delete_instance: self._wrap_async_method( self.delete_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/DeleteInstance", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.wait_operation: self._wrap_method( + self.wait_operation: self._wrap_async_method( self.wait_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/WaitOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py index 2f972ef00317..4543496d4cd9 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.cloud.redis_v1._compat import transcode_request +from google.cloud.redis_v1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -458,6 +459,7 @@ class CloudRedisRestStub: _session: AuthorizedSession _host: str _interceptor: CloudRedisRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class CloudRedisRestTransport(_BaseCloudRedisRestTransport): @@ -505,6 +507,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[CloudRedisRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -548,6 +552,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -559,7 +566,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -637,20 +646,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -717,7 +741,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._CreateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._CreateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -727,7 +761,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_instance_with_metadata(resp, response_metadata) @@ -764,19 +797,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -843,7 +891,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._DeleteInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._DeleteInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -853,7 +910,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_delete_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_delete_instance_with_metadata(resp, response_metadata) @@ -890,19 +946,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -966,7 +1037,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -978,7 +1058,6 @@ def __call__(self, pb_resp = cloud_redis.Instance.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_instance_with_metadata(resp, response_metadata) @@ -1015,19 +1094,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1093,7 +1187,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ListInstances._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._ListInstances._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1105,7 +1208,6 @@ def __call__(self, pb_resp = cloud_redis.ListInstancesResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_instances(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_instances_with_metadata(resp, response_metadata) @@ -1142,20 +1244,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -1222,7 +1339,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._UpdateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._UpdateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1232,7 +1359,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_instance(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_instance_with_metadata(resp, response_metadata) @@ -1263,7 +1389,7 @@ def create_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._CreateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_instance(self) -> Callable[ @@ -1271,7 +1397,7 @@ def delete_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_instance(self) -> Callable[ @@ -1279,7 +1405,7 @@ def get_instance(self) -> Callable[ cloud_redis.Instance]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore + return self._GetInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_instances(self) -> Callable[ @@ -1287,7 +1413,7 @@ def list_instances(self) -> Callable[ cloud_redis.ListInstancesResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore + return self._ListInstances(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_instance(self) -> Callable[ @@ -1295,11 +1421,11 @@ def update_instance(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseCloudRedisRestTransport._BaseGetLocation, CloudRedisRestStub): def __hash__(self): @@ -1313,19 +1439,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1388,7 +1529,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1422,7 +1572,7 @@ def __call__(self, @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseCloudRedisRestTransport._BaseListLocations, CloudRedisRestStub): def __hash__(self): @@ -1436,19 +1586,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1511,7 +1676,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1545,7 +1719,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseCloudRedisRestTransport._BaseCancelOperation, CloudRedisRestStub): def __hash__(self): @@ -1559,19 +1733,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1631,7 +1820,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1642,7 +1840,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseCloudRedisRestTransport._BaseDeleteOperation, CloudRedisRestStub): def __hash__(self): @@ -1656,19 +1854,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1728,7 +1941,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1739,7 +1961,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseCloudRedisRestTransport._BaseGetOperation, CloudRedisRestStub): def __hash__(self): @@ -1753,19 +1975,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -1828,7 +2065,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1862,7 +2108,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseCloudRedisRestTransport._BaseListOperations, CloudRedisRestStub): def __hash__(self): @@ -1876,19 +2122,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -1951,7 +2212,16 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = CloudRedisRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1985,7 +2255,7 @@ def __call__(self, @property def wait_operation(self): - return self._WaitOperation(self._session, self._host, self._interceptor) # type: ignore + return self._WaitOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _WaitOperation(_BaseCloudRedisRestTransport._BaseWaitOperation, CloudRedisRestStub): def __hash__(self): @@ -1999,20 +2269,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -2075,7 +2360,17 @@ def __call__(self, ) # Send the request - response = CloudRedisRestTransport._WaitOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = CloudRedisRestTransport._WaitOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index d79be431bd8a..dfced5164721 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -28,12 +28,13 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore from google.api_core import retry_async as retries from google.api_core import rest_helpers from google.api_core import rest_streaming_async # type: ignore -from google.cloud.redis_v1._compat import transcode_request +from google.cloud.redis_v1._compat import transcode_request, trace_http_request import google.protobuf @@ -472,6 +473,7 @@ class AsyncCloudRedisRestStub: _session: AsyncAuthorizedSession _host: str _interceptor: AsyncCloudRedisRestInterceptor + _client_options: Optional[ClientOptions] = None class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): """Asynchronous REST backend transport for CloudRedis. @@ -511,6 +513,8 @@ def __init__(self, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[AsyncCloudRedisRestInterceptor] = None, + client_options: Optional[ClientOptions] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -536,6 +540,9 @@ def __init__(self, "http" can be specified. interceptor (Optional[AsyncCloudRedisRestInterceptor]): Interceptor used to manipulate requests, request metadata, and responses. + client_options (Optional[google.api_core.client_options.ClientOptions]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor super().__init__( @@ -544,84 +551,95 @@ def __init__(self, client_info=client_info, always_use_jwt_access=False, url_scheme=url_scheme, - api_audience=None + api_audience=None, + client_options=client_options, + **kwargs, ) self._session = AsyncAuthorizedSession(self._credentials) # type: ignore self._interceptor = interceptor or AsyncCloudRedisRestInterceptor() - self._wrap_with_kind = True self._prep_wrapped_messages(client_info) self._operations_client: Optional[operations_v1.AsyncOperationsRestClient] = None def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_instances: self._wrap_method( + self.list_instances: self._wrap_async_method( self.list_instances, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/ListInstances", ), - self.get_instance: self._wrap_method( + self.get_instance: self._wrap_async_method( self.get_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/GetInstance", ), - self.create_instance: self._wrap_method( + self.create_instance: self._wrap_async_method( self.create_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/CreateInstance", ), - self.update_instance: self._wrap_method( + self.update_instance: self._wrap_async_method( self.update_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/UpdateInstance", ), - self.delete_instance: self._wrap_method( + self.delete_instance: self._wrap_async_method( self.delete_instance, default_timeout=600.0, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/DeleteInstance", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.wait_operation: self._wrap_method( + self.wait_operation: self._wrap_async_method( self.wait_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/WaitOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - class _CreateInstance(_BaseCloudRedisRestTransport._BaseCreateInstance, AsyncCloudRedisRestStub): def __hash__(self): return hash("AsyncCloudRedisRestTransport.CreateInstance") @@ -634,20 +652,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -714,7 +747,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._CreateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._CreateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -767,19 +810,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -846,7 +904,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._DeleteInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._DeleteInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -899,19 +966,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -975,7 +1057,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1028,19 +1119,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1106,7 +1212,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ListInstances._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._ListInstances._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1159,20 +1274,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -1239,7 +1369,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._UpdateInstance._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._UpdateInstance._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1340,35 +1480,35 @@ def operations_client(self) -> AsyncOperationsRestClient: def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], operations_pb2.Operation]: - return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._CreateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], operations_pb2.Operation]: - return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], cloud_redis.Instance]: - return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore + return self._GetInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], cloud_redis.ListInstancesResponse]: - return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore + return self._ListInstances(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], operations_pb2.Operation]: - return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateInstance(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseCloudRedisRestTransport._BaseGetLocation, AsyncCloudRedisRestStub): def __hash__(self): @@ -1382,19 +1522,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1457,7 +1612,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1495,7 +1659,7 @@ async def __call__(self, @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseCloudRedisRestTransport._BaseListLocations, AsyncCloudRedisRestStub): def __hash__(self): @@ -1509,19 +1673,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1584,7 +1763,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1622,7 +1810,7 @@ async def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseCloudRedisRestTransport._BaseCancelOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -1636,19 +1824,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1708,7 +1911,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1723,7 +1935,7 @@ async def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseCloudRedisRestTransport._BaseDeleteOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -1737,19 +1949,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1809,7 +2036,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1824,7 +2060,7 @@ async def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseCloudRedisRestTransport._BaseGetOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -1838,19 +2074,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -1913,7 +2164,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1951,7 +2211,7 @@ async def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseCloudRedisRestTransport._BaseListOperations, AsyncCloudRedisRestStub): def __hash__(self): @@ -1965,19 +2225,34 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2040,7 +2315,16 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = await AsyncCloudRedisRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2078,7 +2362,7 @@ async def __call__(self, @property def wait_operation(self): - return self._WaitOperation(self._session, self._host, self._interceptor) # type: ignore + return self._WaitOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _WaitOperation(_BaseCloudRedisRestTransport._BaseWaitOperation, AsyncCloudRedisRestStub): def __hash__(self): @@ -2092,20 +2376,35 @@ async def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response async def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -2168,7 +2467,17 @@ async def __call__(self, ) # Send the request - response = await AsyncCloudRedisRestTransport._WaitOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = await AsyncCloudRedisRestTransport._WaitOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py index 7e9b4428533c..d585f247733b 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.cloud.location import locations_pb2 # type: ignore @@ -49,6 +50,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -69,6 +72,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -84,7 +90,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseCreateInstance: diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 44a69d3d2277..f45bec4b8e3c 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -64,6 +64,7 @@ from google.cloud.redis_v1.services.cloud_redis import CloudRedisClient from google.cloud.redis_v1.services.cloud_redis import pagers from google.cloud.redis_v1.services.cloud_redis import transports +from google.cloud.redis_v1.services.cloud_redis.client import _observability from google.cloud.redis_v1.types import cloud_redis from google.longrunning import operations_pb2 # type: ignore from google.oauth2 import service_account @@ -768,6 +769,192 @@ def test_cloud_redis_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_cloud_redis_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.CloudRedisGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = CloudRedisClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_cloud_redis_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.CloudRedisGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.CloudRedisGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_cloud_redis_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.CloudRedisGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.CloudRedisGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_cloud_redis_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.CloudRedisGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_cloud_redis_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.CloudRedisGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.CloudRedisGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.CloudRedisGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.CloudRedisGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_cloud_redis_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.CloudRedisGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.CloudRedisGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (CloudRedisClient, transports.CloudRedisGrpcTransport, "grpc", grpc_helpers), (CloudRedisAsyncClient, transports.CloudRedisGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py index f7fe8e355981..da6c3dbaaad3 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_compat.py @@ -24,7 +24,17 @@ import google.auth.transport.mtls from google.cloud.redis_v1._compat import transcode_request -from google.cloud.redis_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.redis_v1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +433,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.redis_v1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.redis_v1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py index d7096741a7f9..e9202c61ba46 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/_compat.py @@ -20,18 +20,175 @@ import uuid import google.protobuf.message -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple from typing import TYPE_CHECKING, Union -if TYPE_CHECKING: # pragma: NO COVER - import proto # type: ignore[import-untyped] - +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: NO COVER + import proto # type: ignore[import-untyped] + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/client.py index c8ed5d162686..50586844102c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.showcase_v1beta1._compat import _observability from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -479,18 +480,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., EchoTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py index 84bb83f0a421..684e191c3512 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version +from google.showcase_v1beta1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -56,6 +63,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -86,6 +94,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -123,16 +134,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.echo: gapic_v1.method.wrap_method( + self.echo: self._wrap_method( self.echo, default_retry=retries.Retry( initial=0.1, @@ -146,18 +208,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Echo", ), - self.echo_error_details: gapic_v1.method.wrap_method( + self.echo_error_details: self._wrap_method( self.echo_error_details, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/EchoErrorDetails", ), - self.fail_echo_with_details: gapic_v1.method.wrap_method( + self.fail_echo_with_details: self._wrap_method( self.fail_echo_with_details, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/FailEchoWithDetails", ), - self.expand: gapic_v1.method.wrap_method( + self.expand: self._wrap_method( self.expand, default_retry=retries.Retry( initial=0.1, @@ -171,18 +236,24 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Expand", + is_streaming=True, ), - self.collect: gapic_v1.method.wrap_method( + self.collect: self._wrap_method( self.collect, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Collect", + is_streaming=True, ), - self.chat: gapic_v1.method.wrap_method( + self.chat: self._wrap_method( self.chat, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Chat", + is_streaming=True, ), - self.paged_expand: gapic_v1.method.wrap_method( + self.paged_expand: self._wrap_method( self.paged_expand, default_retry=retries.Retry( initial=0.1, @@ -196,71 +267,85 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/PagedExpand", ), - self.paged_expand_legacy: gapic_v1.method.wrap_method( + self.paged_expand_legacy: self._wrap_method( self.paged_expand_legacy, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/PagedExpandLegacy", ), - self.paged_expand_legacy_mapped: gapic_v1.method.wrap_method( + self.paged_expand_legacy_mapped: self._wrap_method( self.paged_expand_legacy_mapped, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/PagedExpandLegacyMapped", ), - self.wait: gapic_v1.method.wrap_method( + self.wait: self._wrap_method( self.wait, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Wait", ), - self.block: gapic_v1.method.wrap_method( + self.block: self._wrap_method( self.block, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Block", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py index 1134abedc755..4d5fc8e2ebcd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -38,6 +42,10 @@ from google.showcase_v1beta1.types import echo as gs_echo from .base import EchoTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -141,6 +149,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -191,6 +201,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -247,6 +263,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -268,8 +285,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py index 94685df6ac45..c6d94f56b067 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -42,6 +45,7 @@ from .base import EchoTransport, DEFAULT_CLIENT_INFO from .grpc import EchoGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -188,6 +192,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -239,6 +245,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -294,6 +306,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -315,10 +328,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -670,9 +696,12 @@ def block(self) -> Callable[ return self._stubs['block'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.echo: self._wrap_method( + self.echo: self._wrap_async_method( self.echo, default_retry=retries.AsyncRetry( initial=0.1, @@ -686,18 +715,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Echo", ), - self.echo_error_details: self._wrap_method( + self.echo_error_details: self._wrap_async_method( self.echo_error_details, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/EchoErrorDetails", ), - self.fail_echo_with_details: self._wrap_method( + self.fail_echo_with_details: self._wrap_async_method( self.fail_echo_with_details, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/FailEchoWithDetails", ), - self.expand: self._wrap_method( + self.expand: self._wrap_async_method( self.expand, default_retry=retries.AsyncRetry( initial=0.1, @@ -711,18 +743,24 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Expand", + is_streaming=True, ), - self.collect: self._wrap_method( + self.collect: self._wrap_async_method( self.collect, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Collect", + is_streaming=True, ), - self.chat: self._wrap_method( + self.chat: self._wrap_async_method( self.chat, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Chat", + is_streaming=True, ), - self.paged_expand: self._wrap_method( + self.paged_expand: self._wrap_async_method( self.paged_expand, default_retry=retries.AsyncRetry( initial=0.1, @@ -736,79 +774,88 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/PagedExpand", ), - self.paged_expand_legacy: self._wrap_method( + self.paged_expand_legacy: self._wrap_async_method( self.paged_expand_legacy, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/PagedExpandLegacy", ), - self.paged_expand_legacy_mapped: self._wrap_method( + self.paged_expand_legacy_mapped: self._wrap_async_method( self.paged_expand_legacy_mapped, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/PagedExpandLegacyMapped", ), - self.wait: self._wrap_method( + self.wait: self._wrap_async_method( self.wait, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Wait", ), - self.block: self._wrap_method( + self.block: self._wrap_async_method( self.block, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Echo/Block", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest.py index 9eceb532293c..0608feae5957 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -678,6 +679,7 @@ class EchoRestStub: _session: AuthorizedSession _host: str _interceptor: EchoRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class EchoRestTransport(_BaseEchoRestTransport): @@ -712,6 +714,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[EchoRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -751,6 +755,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -762,7 +769,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -833,20 +842,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.BlockRequest, *, @@ -909,7 +933,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._Block._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._Block._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -921,7 +955,6 @@ def __call__(self, pb_resp = gs_echo.BlockResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_block(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_block_with_metadata(resp, response_metadata) @@ -984,20 +1017,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.EchoRequest, *, @@ -1067,7 +1115,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._Echo._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._Echo._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1079,7 +1137,6 @@ def __call__(self, pb_resp = gs_echo.EchoResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_echo(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_echo_with_metadata(resp, response_metadata) @@ -1116,20 +1173,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.EchoErrorDetailsRequest, *, @@ -1195,7 +1267,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._EchoErrorDetails._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._EchoErrorDetails._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1207,7 +1289,6 @@ def __call__(self, pb_resp = gs_echo.EchoErrorDetailsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_echo_error_details(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_echo_error_details_with_metadata(resp, response_metadata) @@ -1244,21 +1325,36 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - stream=True, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + stream=True, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.ExpandRequest, *, @@ -1324,7 +1420,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._Expand._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._Expand._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1333,7 +1439,6 @@ def __call__(self, # Return the response resp = rest_streaming.ResponseIterator(response, gs_echo.EchoResponse) - resp = self._interceptor.post_expand(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_expand_with_metadata(resp, response_metadata) @@ -1365,20 +1470,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.FailEchoWithDetailsRequest, *, @@ -1445,7 +1565,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._FailEchoWithDetails._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._FailEchoWithDetails._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1457,7 +1587,6 @@ def __call__(self, pb_resp = gs_echo.FailEchoWithDetailsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_fail_echo_with_details(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_fail_echo_with_details_with_metadata(resp, response_metadata) @@ -1494,20 +1623,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.PagedExpandRequest, *, @@ -1573,7 +1717,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._PagedExpand._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._PagedExpand._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1585,7 +1739,6 @@ def __call__(self, pb_resp = gs_echo.PagedExpandResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_paged_expand(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_paged_expand_with_metadata(resp, response_metadata) @@ -1622,20 +1775,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.PagedExpandLegacyRequest, *, @@ -1705,7 +1873,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._PagedExpandLegacy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._PagedExpandLegacy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1717,7 +1895,6 @@ def __call__(self, pb_resp = gs_echo.PagedExpandResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_paged_expand_legacy(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_paged_expand_legacy_with_metadata(resp, response_metadata) @@ -1754,20 +1931,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.PagedExpandRequest, *, @@ -1832,7 +2024,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._PagedExpandLegacyMapped._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._PagedExpandLegacyMapped._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1844,7 +2046,6 @@ def __call__(self, pb_resp = gs_echo.PagedExpandLegacyMappedResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_paged_expand_legacy_mapped(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_paged_expand_legacy_mapped_with_metadata(resp, response_metadata) @@ -1881,20 +2082,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_echo.WaitRequest, *, @@ -1960,7 +2176,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._Wait._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._Wait._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1970,7 +2196,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_wait(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_wait_with_metadata(resp, response_metadata) @@ -2001,7 +2226,7 @@ def block(self) -> Callable[ gs_echo.BlockResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Block(self._session, self._host, self._interceptor) # type: ignore + return self._Block(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def chat(self) -> Callable[ @@ -2009,7 +2234,7 @@ def chat(self) -> Callable[ gs_echo.EchoResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Chat(self._session, self._host, self._interceptor) # type: ignore + return self._Chat(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def collect(self) -> Callable[ @@ -2017,7 +2242,7 @@ def collect(self) -> Callable[ gs_echo.EchoResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Collect(self._session, self._host, self._interceptor) # type: ignore + return self._Collect(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def echo(self) -> Callable[ @@ -2025,7 +2250,7 @@ def echo(self) -> Callable[ gs_echo.EchoResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Echo(self._session, self._host, self._interceptor) # type: ignore + return self._Echo(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def echo_error_details(self) -> Callable[ @@ -2033,7 +2258,7 @@ def echo_error_details(self) -> Callable[ gs_echo.EchoErrorDetailsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._EchoErrorDetails(self._session, self._host, self._interceptor) # type: ignore + return self._EchoErrorDetails(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def expand(self) -> Callable[ @@ -2041,7 +2266,7 @@ def expand(self) -> Callable[ gs_echo.EchoResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Expand(self._session, self._host, self._interceptor) # type: ignore + return self._Expand(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def fail_echo_with_details(self) -> Callable[ @@ -2049,7 +2274,7 @@ def fail_echo_with_details(self) -> Callable[ gs_echo.FailEchoWithDetailsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._FailEchoWithDetails(self._session, self._host, self._interceptor) # type: ignore + return self._FailEchoWithDetails(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def paged_expand(self) -> Callable[ @@ -2057,7 +2282,7 @@ def paged_expand(self) -> Callable[ gs_echo.PagedExpandResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._PagedExpand(self._session, self._host, self._interceptor) # type: ignore + return self._PagedExpand(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def paged_expand_legacy(self) -> Callable[ @@ -2065,7 +2290,7 @@ def paged_expand_legacy(self) -> Callable[ gs_echo.PagedExpandResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._PagedExpandLegacy(self._session, self._host, self._interceptor) # type: ignore + return self._PagedExpandLegacy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def paged_expand_legacy_mapped(self) -> Callable[ @@ -2073,7 +2298,7 @@ def paged_expand_legacy_mapped(self) -> Callable[ gs_echo.PagedExpandLegacyMappedResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._PagedExpandLegacyMapped(self._session, self._host, self._interceptor) # type: ignore + return self._PagedExpandLegacyMapped(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def wait(self) -> Callable[ @@ -2081,11 +2306,11 @@ def wait(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Wait(self._session, self._host, self._interceptor) # type: ignore + return self._Wait(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseEchoRestTransport._BaseListLocations, EchoRestStub): def __hash__(self): @@ -2099,19 +2324,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -2174,7 +2414,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2208,7 +2457,7 @@ def __call__(self, @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseEchoRestTransport._BaseGetLocation, EchoRestStub): def __hash__(self): @@ -2222,19 +2471,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -2297,7 +2561,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2331,7 +2604,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseEchoRestTransport._BaseSetIamPolicy, EchoRestStub): def __hash__(self): @@ -2345,20 +2618,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -2421,7 +2709,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2455,7 +2753,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseEchoRestTransport._BaseGetIamPolicy, EchoRestStub): def __hash__(self): @@ -2469,19 +2767,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -2544,7 +2857,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2578,7 +2900,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseEchoRestTransport._BaseTestIamPermissions, EchoRestStub): def __hash__(self): @@ -2592,20 +2914,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -2668,7 +3005,17 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = EchoRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2702,7 +3049,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseEchoRestTransport._BaseListOperations, EchoRestStub): def __hash__(self): @@ -2716,19 +3063,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2791,7 +3153,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2825,7 +3196,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseEchoRestTransport._BaseGetOperation, EchoRestStub): def __hash__(self): @@ -2839,19 +3210,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2914,7 +3300,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2948,7 +3343,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseEchoRestTransport._BaseDeleteOperation, EchoRestStub): def __hash__(self): @@ -2962,19 +3357,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -3034,7 +3444,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3045,7 +3464,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseEchoRestTransport._BaseCancelOperation, EchoRestStub): def __hash__(self): @@ -3059,19 +3478,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -3131,7 +3565,16 @@ def __call__(self, ) # Send the request - response = EchoRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = EchoRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest_base.py index 35ef144c12ea..fdcafa2f4288 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -51,6 +52,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -71,6 +74,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -86,7 +92,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseBlock: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/client.py index f47890a9bb42..5ecb1f9fa8f7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.showcase_v1beta1._compat import _observability from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -475,18 +476,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., IdentityTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py index 3d8c1973d646..5bf22ac55612 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version +from google.showcase_v1beta1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -56,6 +63,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -86,6 +94,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -123,21 +134,73 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.create_user: gapic_v1.method.wrap_method( + self.create_user: self._wrap_method( self.create_user, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/CreateUser", ), - self.get_user: gapic_v1.method.wrap_method( + self.get_user: self._wrap_method( self.get_user, default_retry=retries.Retry( initial=0.2, @@ -151,18 +214,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=5.0, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/GetUser", ), - self.update_user: gapic_v1.method.wrap_method( + self.update_user: self._wrap_method( self.update_user, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/UpdateUser", ), - self.delete_user: gapic_v1.method.wrap_method( + self.delete_user: self._wrap_method( self.delete_user, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/DeleteUser", ), - self.list_users: gapic_v1.method.wrap_method( + self.list_users: self._wrap_method( self.list_users, default_retry=retries.Retry( initial=0.2, @@ -176,51 +242,61 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=5.0, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/ListUsers", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py index 332e019bd35c..3a72ba37662e 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -38,6 +42,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import IdentityTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -134,6 +142,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -184,6 +194,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -239,6 +255,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -260,8 +277,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py index 0b2ce53e949b..299e009966a7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -42,6 +45,7 @@ from .base import IdentityTransport, DEFAULT_CLIENT_INFO from .grpc import IdentityGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -181,6 +185,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -232,6 +238,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -286,6 +298,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -307,10 +320,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -456,14 +482,18 @@ def list_users(self) -> Callable[ return self._stubs['list_users'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.create_user: self._wrap_method( + self.create_user: self._wrap_async_method( self.create_user, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/CreateUser", ), - self.get_user: self._wrap_method( + self.get_user: self._wrap_async_method( self.get_user, default_retry=retries.AsyncRetry( initial=0.2, @@ -477,18 +507,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=5.0, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/GetUser", ), - self.update_user: self._wrap_method( + self.update_user: self._wrap_async_method( self.update_user, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/UpdateUser", ), - self.delete_user: self._wrap_method( + self.delete_user: self._wrap_async_method( self.delete_user, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/DeleteUser", ), - self.list_users: self._wrap_method( + self.list_users: self._wrap_async_method( self.list_users, default_retry=retries.AsyncRetry( initial=0.2, @@ -502,59 +535,64 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=5.0, client_info=client_info, + method_name="google.showcase.v1beta1.Identity/ListUsers", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest.py index 06f43bca8c9f..1ff8e157b8d8 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -470,6 +471,7 @@ class IdentityRestStub: _session: AuthorizedSession _host: str _interceptor: IdentityRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class IdentityRestTransport(_BaseIdentityRestTransport): @@ -497,6 +499,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[IdentityRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -536,6 +540,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -547,7 +554,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -568,20 +577,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: identity.CreateUserRequest, *, @@ -646,7 +670,17 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._CreateUser._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IdentityRestTransport._CreateUser._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -658,7 +692,6 @@ def __call__(self, pb_resp = identity.User.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_user(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_user_with_metadata(resp, response_metadata) @@ -695,19 +728,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: identity.DeleteUserRequest, *, @@ -768,7 +816,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._DeleteUser._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._DeleteUser._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -787,19 +844,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: identity.GetUserRequest, *, @@ -864,7 +936,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._GetUser._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._GetUser._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -876,7 +957,6 @@ def __call__(self, pb_resp = identity.User.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_user(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_user_with_metadata(resp, response_metadata) @@ -913,19 +993,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: identity.ListUsersRequest, *, @@ -993,7 +1088,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._ListUsers._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._ListUsers._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1005,7 +1109,6 @@ def __call__(self, pb_resp = identity.ListUsersResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_users(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_users_with_metadata(resp, response_metadata) @@ -1042,20 +1145,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: identity.UpdateUserRequest, *, @@ -1120,7 +1238,17 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._UpdateUser._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IdentityRestTransport._UpdateUser._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1132,7 +1260,6 @@ def __call__(self, pb_resp = identity.User.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_user(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_user_with_metadata(resp, response_metadata) @@ -1163,7 +1290,7 @@ def create_user(self) -> Callable[ identity.User]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateUser(self._session, self._host, self._interceptor) # type: ignore + return self._CreateUser(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_user(self) -> Callable[ @@ -1171,7 +1298,7 @@ def delete_user(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteUser(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteUser(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_user(self) -> Callable[ @@ -1179,7 +1306,7 @@ def get_user(self) -> Callable[ identity.User]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetUser(self._session, self._host, self._interceptor) # type: ignore + return self._GetUser(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_users(self) -> Callable[ @@ -1187,7 +1314,7 @@ def list_users(self) -> Callable[ identity.ListUsersResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListUsers(self._session, self._host, self._interceptor) # type: ignore + return self._ListUsers(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_user(self) -> Callable[ @@ -1195,11 +1322,11 @@ def update_user(self) -> Callable[ identity.User]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateUser(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateUser(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseIdentityRestTransport._BaseListLocations, IdentityRestStub): def __hash__(self): @@ -1213,19 +1340,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1288,7 +1430,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1322,7 +1473,7 @@ def __call__(self, @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseIdentityRestTransport._BaseGetLocation, IdentityRestStub): def __hash__(self): @@ -1336,19 +1487,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1411,7 +1577,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1445,7 +1620,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseIdentityRestTransport._BaseSetIamPolicy, IdentityRestStub): def __hash__(self): @@ -1459,20 +1634,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -1535,7 +1725,17 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IdentityRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1569,7 +1769,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseIdentityRestTransport._BaseGetIamPolicy, IdentityRestStub): def __hash__(self): @@ -1583,19 +1783,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -1658,7 +1873,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1692,7 +1916,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseIdentityRestTransport._BaseTestIamPermissions, IdentityRestStub): def __hash__(self): @@ -1706,20 +1930,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -1782,7 +2021,17 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = IdentityRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1816,7 +2065,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseIdentityRestTransport._BaseListOperations, IdentityRestStub): def __hash__(self): @@ -1830,19 +2079,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -1905,7 +2169,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1939,7 +2212,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseIdentityRestTransport._BaseGetOperation, IdentityRestStub): def __hash__(self): @@ -1953,19 +2226,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2028,7 +2316,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2062,7 +2359,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseIdentityRestTransport._BaseDeleteOperation, IdentityRestStub): def __hash__(self): @@ -2076,19 +2373,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2148,7 +2460,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2159,7 +2480,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseIdentityRestTransport._BaseCancelOperation, IdentityRestStub): def __hash__(self): @@ -2173,19 +2494,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2245,7 +2581,16 @@ def __call__(self, ) # Send the request - response = IdentityRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = IdentityRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest_base.py index a273a518fbc3..2740986c9745 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -52,6 +53,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -72,6 +75,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -87,7 +93,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseCreateUser: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/client.py index 919ef806ecb2..e27cf20ee69e 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.showcase_v1beta1._compat import _observability from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -503,18 +504,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., MessagingTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py index 56895799a573..4078f037702f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version +from google.showcase_v1beta1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -57,6 +64,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -87,6 +95,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -124,21 +135,73 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.create_room: gapic_v1.method.wrap_method( + self.create_room: self._wrap_method( self.create_room, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/CreateRoom", ), - self.get_room: gapic_v1.method.wrap_method( + self.get_room: self._wrap_method( self.get_room, default_retry=retries.Retry( initial=0.1, @@ -152,18 +215,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/GetRoom", ), - self.update_room: gapic_v1.method.wrap_method( + self.update_room: self._wrap_method( self.update_room, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/UpdateRoom", ), - self.delete_room: gapic_v1.method.wrap_method( + self.delete_room: self._wrap_method( self.delete_room, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/DeleteRoom", ), - self.list_rooms: gapic_v1.method.wrap_method( + self.list_rooms: self._wrap_method( self.list_rooms, default_retry=retries.Retry( initial=0.1, @@ -177,13 +243,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/ListRooms", ), - self.create_blurb: gapic_v1.method.wrap_method( + self.create_blurb: self._wrap_method( self.create_blurb, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/CreateBlurb", ), - self.get_blurb: gapic_v1.method.wrap_method( + self.get_blurb: self._wrap_method( self.get_blurb, default_retry=retries.Retry( initial=0.1, @@ -197,18 +265,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/GetBlurb", ), - self.update_blurb: gapic_v1.method.wrap_method( + self.update_blurb: self._wrap_method( self.update_blurb, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/UpdateBlurb", ), - self.delete_blurb: gapic_v1.method.wrap_method( + self.delete_blurb: self._wrap_method( self.delete_blurb, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/DeleteBlurb", ), - self.list_blurbs: gapic_v1.method.wrap_method( + self.list_blurbs: self._wrap_method( self.list_blurbs, default_retry=retries.Retry( initial=0.1, @@ -222,8 +293,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/ListBlurbs", ), - self.search_blurbs: gapic_v1.method.wrap_method( + self.search_blurbs: self._wrap_method( self.search_blurbs, default_retry=retries.Retry( initial=0.1, @@ -237,18 +309,23 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/SearchBlurbs", ), - self.stream_blurbs: gapic_v1.method.wrap_method( + self.stream_blurbs: self._wrap_method( self.stream_blurbs, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/StreamBlurbs", + is_streaming=True, ), - self.send_blurbs: gapic_v1.method.wrap_method( + self.send_blurbs: self._wrap_method( self.send_blurbs, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/SendBlurbs", + is_streaming=True, ), - self.connect: gapic_v1.method.wrap_method( + self.connect: self._wrap_method( self.connect, default_retry=retries.Retry( initial=0.1, @@ -262,51 +339,62 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/Connect", + is_streaming=True, ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py index 4d747c43ed51..4da466085716 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -39,6 +43,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import MessagingTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -138,6 +146,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -188,6 +198,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -244,6 +260,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -265,8 +282,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py index 0011029fc37e..dc90eb327bd9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -43,6 +46,7 @@ from .base import MessagingTransport, DEFAULT_CLIENT_INFO from .grpc import MessagingGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -185,6 +189,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -236,6 +242,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -291,6 +303,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -312,10 +325,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -724,14 +750,18 @@ def connect(self) -> Callable[ return self._stubs['connect'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.create_room: self._wrap_method( + self.create_room: self._wrap_async_method( self.create_room, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/CreateRoom", ), - self.get_room: self._wrap_method( + self.get_room: self._wrap_async_method( self.get_room, default_retry=retries.AsyncRetry( initial=0.1, @@ -745,18 +775,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/GetRoom", ), - self.update_room: self._wrap_method( + self.update_room: self._wrap_async_method( self.update_room, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/UpdateRoom", ), - self.delete_room: self._wrap_method( + self.delete_room: self._wrap_async_method( self.delete_room, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/DeleteRoom", ), - self.list_rooms: self._wrap_method( + self.list_rooms: self._wrap_async_method( self.list_rooms, default_retry=retries.AsyncRetry( initial=0.1, @@ -770,13 +803,15 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/ListRooms", ), - self.create_blurb: self._wrap_method( + self.create_blurb: self._wrap_async_method( self.create_blurb, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/CreateBlurb", ), - self.get_blurb: self._wrap_method( + self.get_blurb: self._wrap_async_method( self.get_blurb, default_retry=retries.AsyncRetry( initial=0.1, @@ -790,18 +825,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/GetBlurb", ), - self.update_blurb: self._wrap_method( + self.update_blurb: self._wrap_async_method( self.update_blurb, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/UpdateBlurb", ), - self.delete_blurb: self._wrap_method( + self.delete_blurb: self._wrap_async_method( self.delete_blurb, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/DeleteBlurb", ), - self.list_blurbs: self._wrap_method( + self.list_blurbs: self._wrap_async_method( self.list_blurbs, default_retry=retries.AsyncRetry( initial=0.1, @@ -815,8 +853,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/ListBlurbs", ), - self.search_blurbs: self._wrap_method( + self.search_blurbs: self._wrap_async_method( self.search_blurbs, default_retry=retries.AsyncRetry( initial=0.1, @@ -830,18 +869,23 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/SearchBlurbs", ), - self.stream_blurbs: self._wrap_method( + self.stream_blurbs: self._wrap_async_method( self.stream_blurbs, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/StreamBlurbs", + is_streaming=True, ), - self.send_blurbs: self._wrap_method( + self.send_blurbs: self._wrap_async_method( self.send_blurbs, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/SendBlurbs", + is_streaming=True, ), - self.connect: self._wrap_method( + self.connect: self._wrap_async_method( self.connect, default_retry=retries.AsyncRetry( initial=0.1, @@ -855,59 +899,65 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.Messaging/Connect", + is_streaming=True, ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest.py index 1695f0987347..0cfa8094cec7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -747,6 +748,7 @@ class MessagingRestStub: _session: AuthorizedSession _host: str _interceptor: MessagingRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class MessagingRestTransport(_BaseMessagingRestTransport): @@ -777,6 +779,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[MessagingRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -816,6 +820,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -827,7 +834,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -911,20 +920,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.CreateBlurbRequest, *, @@ -992,7 +1016,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._CreateBlurb._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._CreateBlurb._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1004,7 +1038,6 @@ def __call__(self, pb_resp = messaging.Blurb.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_blurb(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_blurb_with_metadata(resp, response_metadata) @@ -1041,20 +1074,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.CreateRoomRequest, *, @@ -1119,7 +1167,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._CreateRoom._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._CreateRoom._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1131,7 +1189,6 @@ def __call__(self, pb_resp = messaging.Room.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_room(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_room_with_metadata(resp, response_metadata) @@ -1168,19 +1225,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.DeleteBlurbRequest, *, @@ -1241,7 +1313,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._DeleteBlurb._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._DeleteBlurb._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1260,19 +1341,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.DeleteRoomRequest, *, @@ -1333,7 +1429,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._DeleteRoom._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._DeleteRoom._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1352,19 +1457,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.GetBlurbRequest, *, @@ -1432,7 +1552,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._GetBlurb._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._GetBlurb._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1444,7 +1573,6 @@ def __call__(self, pb_resp = messaging.Blurb.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_blurb(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_blurb_with_metadata(resp, response_metadata) @@ -1481,19 +1609,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.GetRoomRequest, *, @@ -1558,7 +1701,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._GetRoom._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._GetRoom._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1570,7 +1722,6 @@ def __call__(self, pb_resp = messaging.Room.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_room(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_room_with_metadata(resp, response_metadata) @@ -1607,19 +1758,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.ListBlurbsRequest, *, @@ -1687,7 +1853,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._ListBlurbs._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._ListBlurbs._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1699,7 +1874,6 @@ def __call__(self, pb_resp = messaging.ListBlurbsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_blurbs(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_blurbs_with_metadata(resp, response_metadata) @@ -1736,19 +1910,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.ListRoomsRequest, *, @@ -1816,7 +2005,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._ListRooms._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._ListRooms._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1828,7 +2026,6 @@ def __call__(self, pb_resp = messaging.ListRoomsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_rooms(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_rooms_with_metadata(resp, response_metadata) @@ -1865,20 +2062,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.SearchBlurbsRequest, *, @@ -1946,7 +2158,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._SearchBlurbs._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._SearchBlurbs._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1956,7 +2178,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_search_blurbs(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_search_blurbs_with_metadata(resp, response_metadata) @@ -2006,21 +2227,36 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - stream=True, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + stream=True, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.StreamBlurbsRequest, *, @@ -2088,7 +2324,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._StreamBlurbs._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._StreamBlurbs._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2097,7 +2343,6 @@ def __call__(self, # Return the response resp = rest_streaming.ResponseIterator(response, messaging.StreamBlurbsResponse) - resp = self._interceptor.post_stream_blurbs(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_stream_blurbs_with_metadata(resp, response_metadata) @@ -2129,20 +2374,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.UpdateBlurbRequest, *, @@ -2210,7 +2470,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._UpdateBlurb._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._UpdateBlurb._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2222,7 +2492,6 @@ def __call__(self, pb_resp = messaging.Blurb.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_blurb(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_blurb_with_metadata(resp, response_metadata) @@ -2259,20 +2528,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: messaging.UpdateRoomRequest, *, @@ -2337,7 +2621,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._UpdateRoom._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._UpdateRoom._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2349,7 +2643,6 @@ def __call__(self, pb_resp = messaging.Room.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_update_room(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_update_room_with_metadata(resp, response_metadata) @@ -2380,7 +2673,7 @@ def connect(self) -> Callable[ messaging.StreamBlurbsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._Connect(self._session, self._host, self._interceptor) # type: ignore + return self._Connect(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_blurb(self) -> Callable[ @@ -2388,7 +2681,7 @@ def create_blurb(self) -> Callable[ messaging.Blurb]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateBlurb(self._session, self._host, self._interceptor) # type: ignore + return self._CreateBlurb(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_room(self) -> Callable[ @@ -2396,7 +2689,7 @@ def create_room(self) -> Callable[ messaging.Room]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateRoom(self._session, self._host, self._interceptor) # type: ignore + return self._CreateRoom(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_blurb(self) -> Callable[ @@ -2404,7 +2697,7 @@ def delete_blurb(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteBlurb(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteBlurb(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_room(self) -> Callable[ @@ -2412,7 +2705,7 @@ def delete_room(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteRoom(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteRoom(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_blurb(self) -> Callable[ @@ -2420,7 +2713,7 @@ def get_blurb(self) -> Callable[ messaging.Blurb]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetBlurb(self._session, self._host, self._interceptor) # type: ignore + return self._GetBlurb(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_room(self) -> Callable[ @@ -2428,7 +2721,7 @@ def get_room(self) -> Callable[ messaging.Room]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetRoom(self._session, self._host, self._interceptor) # type: ignore + return self._GetRoom(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_blurbs(self) -> Callable[ @@ -2436,7 +2729,7 @@ def list_blurbs(self) -> Callable[ messaging.ListBlurbsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListBlurbs(self._session, self._host, self._interceptor) # type: ignore + return self._ListBlurbs(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_rooms(self) -> Callable[ @@ -2444,7 +2737,7 @@ def list_rooms(self) -> Callable[ messaging.ListRoomsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListRooms(self._session, self._host, self._interceptor) # type: ignore + return self._ListRooms(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def search_blurbs(self) -> Callable[ @@ -2452,7 +2745,7 @@ def search_blurbs(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._SearchBlurbs(self._session, self._host, self._interceptor) # type: ignore + return self._SearchBlurbs(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def send_blurbs(self) -> Callable[ @@ -2460,7 +2753,7 @@ def send_blurbs(self) -> Callable[ messaging.SendBlurbsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._SendBlurbs(self._session, self._host, self._interceptor) # type: ignore + return self._SendBlurbs(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def stream_blurbs(self) -> Callable[ @@ -2468,7 +2761,7 @@ def stream_blurbs(self) -> Callable[ messaging.StreamBlurbsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._StreamBlurbs(self._session, self._host, self._interceptor) # type: ignore + return self._StreamBlurbs(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_blurb(self) -> Callable[ @@ -2476,7 +2769,7 @@ def update_blurb(self) -> Callable[ messaging.Blurb]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateBlurb(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateBlurb(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def update_room(self) -> Callable[ @@ -2484,11 +2777,11 @@ def update_room(self) -> Callable[ messaging.Room]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UpdateRoom(self._session, self._host, self._interceptor) # type: ignore + return self._UpdateRoom(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseMessagingRestTransport._BaseListLocations, MessagingRestStub): def __hash__(self): @@ -2502,19 +2795,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -2577,7 +2885,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2611,7 +2928,7 @@ def __call__(self, @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseMessagingRestTransport._BaseGetLocation, MessagingRestStub): def __hash__(self): @@ -2625,19 +2942,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -2700,7 +3032,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2734,7 +3075,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseMessagingRestTransport._BaseSetIamPolicy, MessagingRestStub): def __hash__(self): @@ -2748,20 +3089,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -2824,7 +3180,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2858,7 +3224,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseMessagingRestTransport._BaseGetIamPolicy, MessagingRestStub): def __hash__(self): @@ -2872,19 +3238,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -2947,7 +3328,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2981,7 +3371,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseMessagingRestTransport._BaseTestIamPermissions, MessagingRestStub): def __hash__(self): @@ -2995,20 +3385,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -3071,7 +3476,17 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = MessagingRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3105,7 +3520,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseMessagingRestTransport._BaseListOperations, MessagingRestStub): def __hash__(self): @@ -3119,19 +3534,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -3194,7 +3624,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3228,7 +3667,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseMessagingRestTransport._BaseGetOperation, MessagingRestStub): def __hash__(self): @@ -3242,19 +3681,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -3317,7 +3771,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3351,7 +3814,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseMessagingRestTransport._BaseDeleteOperation, MessagingRestStub): def __hash__(self): @@ -3365,19 +3828,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -3437,7 +3915,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3448,7 +3935,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseMessagingRestTransport._BaseCancelOperation, MessagingRestStub): def __hash__(self): @@ -3462,19 +3949,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -3534,7 +4036,16 @@ def __call__(self, ) # Send the request - response = MessagingRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = MessagingRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest_base.py index 8333cfaf4ea7..fd5e21a2deb9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -52,6 +53,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -72,6 +75,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -87,7 +93,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseConnect: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index 1f62d75b9951..3697975f1947 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -29,6 +29,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core.resumable_transfer import ResumableUploadConfig +from google.showcase_v1beta1._compat import _observability from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -466,18 +467,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., ResumableUploadServiceTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 0009a9c91cbb..b1688f42550a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version +from google.showcase_v1beta1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -56,6 +63,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -86,6 +94,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -124,64 +135,125 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.upload_media: gapic_v1.method.wrap_method( + self.upload_media: self._wrap_method( self.upload_media, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.ResumableUploadService/UploadMedia", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index dd52dbc790da..164281da93ae 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import resumable_transfer from google.api_core import grpc_helpers @@ -40,6 +44,10 @@ from .base import ResumableUploadServiceTransport, DEFAULT_CLIENT_INFO from .rest import ResumableUploadServiceRestTransport +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -137,6 +145,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -187,6 +197,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -249,6 +265,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -270,8 +287,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 40782c9a3092..db2b68ba76be 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Any, Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -47,6 +50,7 @@ except ImportError: # pragma: NO COVER HAS_ASYNC_REST = False + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -187,6 +191,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -238,6 +244,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -299,6 +311,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -320,10 +333,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -400,65 +426,73 @@ def __call__(self, *args, **kwargs): return self._stubs['upload_media'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.upload_media: self._wrap_method( + self.upload_media: self._wrap_async_method( self.upload_media, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.ResumableUploadService/UploadMedia", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - async def close(self): await self._logged_channel.close() if self._rest_transport is not None: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 032336df4352..40cdeb436e98 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -18,6 +18,7 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers @@ -25,7 +26,7 @@ from google.api_core import gapic_v1 import urllib.parse from google.api_core import resumable_transfer -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -295,6 +296,7 @@ class ResumableUploadServiceRestStub: _session: AuthorizedSession _host: str _interceptor: ResumableUploadServiceRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class ResumableUploadServiceRestTransport(_BaseResumableUploadServiceRestTransport): @@ -323,6 +325,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[ResumableUploadServiceRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -362,6 +366,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -373,7 +380,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -462,11 +471,11 @@ def upload_media(self) -> Callable[ resumable_transfer.ResumableUploadSession]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore + return self._UploadMedia(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseResumableUploadServiceRestTransport._BaseListLocations, ResumableUploadServiceRestStub): def __hash__(self): @@ -480,19 +489,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -555,7 +579,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -589,7 +622,7 @@ def __call__(self, @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseResumableUploadServiceRestTransport._BaseGetLocation, ResumableUploadServiceRestStub): def __hash__(self): @@ -603,19 +636,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -678,7 +726,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -712,7 +769,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseResumableUploadServiceRestTransport._BaseSetIamPolicy, ResumableUploadServiceRestStub): def __hash__(self): @@ -726,20 +783,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -802,7 +874,17 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = ResumableUploadServiceRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -836,7 +918,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseResumableUploadServiceRestTransport._BaseGetIamPolicy, ResumableUploadServiceRestStub): def __hash__(self): @@ -850,19 +932,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -925,7 +1022,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -959,7 +1065,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseResumableUploadServiceRestTransport._BaseTestIamPermissions, ResumableUploadServiceRestStub): def __hash__(self): @@ -973,20 +1079,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -1049,7 +1170,17 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = ResumableUploadServiceRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1083,7 +1214,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseResumableUploadServiceRestTransport._BaseListOperations, ResumableUploadServiceRestStub): def __hash__(self): @@ -1097,19 +1228,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -1172,7 +1318,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1206,7 +1361,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseResumableUploadServiceRestTransport._BaseGetOperation, ResumableUploadServiceRestStub): def __hash__(self): @@ -1220,19 +1375,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -1295,7 +1465,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1329,7 +1508,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseResumableUploadServiceRestTransport._BaseDeleteOperation, ResumableUploadServiceRestStub): def __hash__(self): @@ -1343,19 +1522,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1415,7 +1609,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1426,7 +1629,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseResumableUploadServiceRestTransport._BaseCancelOperation, ResumableUploadServiceRestStub): def __hash__(self): @@ -1440,19 +1643,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1512,7 +1730,16 @@ def __call__(self, ) # Send the request - response = ResumableUploadServiceRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = ResumableUploadServiceRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index fa206afac970..2860f7933e4c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -28,6 +28,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions import urllib.parse from google.api_core import resumable_transfer from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -36,7 +37,7 @@ from google.api_core import retry_async as retries from google.api_core import rest_helpers from google.api_core import rest_streaming_async # type: ignore -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf @@ -121,6 +122,7 @@ class AsyncResumableUploadServiceRestStub: _session: AsyncAuthorizedSession _host: str _interceptor: AsyncResumableUploadServiceRestInterceptor + _client_options: Optional[ClientOptions] = None class AsyncResumableUploadServiceRestTransport(_BaseResumableUploadServiceRestTransport): """Asynchronous REST backend transport for ResumableUploadService. @@ -141,6 +143,8 @@ def __init__(self, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[AsyncResumableUploadServiceRestInterceptor] = None, + client_options: Optional[ClientOptions] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -162,6 +166,9 @@ def __init__(self, "http" can be specified. interceptor (Optional[AsyncResumableUploadServiceRestInterceptor]): Interceptor used to manipulate requests, request metadata, and responses. + client_options (Optional[google.api_core.client_options.ClientOptions]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor super().__init__( @@ -170,28 +177,28 @@ def __init__(self, client_info=client_info, always_use_jwt_access=False, url_scheme=url_scheme, - api_audience=None + api_audience=None, + client_options=client_options, + **kwargs, ) self._session = AsyncAuthorizedSession(self._credentials) # type: ignore self._interceptor = interceptor or AsyncResumableUploadServiceRestInterceptor() - self._wrap_with_kind = True self._prep_wrapped_messages(client_info) def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.upload_media: self._wrap_method( + self.upload_media: self._wrap_async_method( self.upload_media, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.ResumableUploadService/UploadMedia", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - class _UploadMedia(_BaseResumableUploadServiceRestTransport._BaseUploadMedia, AsyncResumableUploadServiceRestStub): def __hash__(self): return hash("AsyncResumableUploadServiceRestTransport.UploadMedia") @@ -270,7 +277,7 @@ async def __call__(self, def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], resumable_transfer.AsyncResumableUploadSession]: - return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore + return self._UploadMedia(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def kind(self) -> str: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py index 207171c714db..7fe172ec6064 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -51,6 +52,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -71,6 +74,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -86,7 +92,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseUploadMedia: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/client.py index f408f534f317..ec443356c2bf 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.showcase_v1beta1._compat import _observability from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -510,18 +511,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., SequenceServiceTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py index c3874945257d..a82c31ac851c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version +from google.showcase_v1beta1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -57,6 +64,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -87,6 +95,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -124,36 +135,91 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.create_sequence: gapic_v1.method.wrap_method( + self.create_sequence: self._wrap_method( self.create_sequence, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/CreateSequence", ), - self.create_streaming_sequence: gapic_v1.method.wrap_method( + self.create_streaming_sequence: self._wrap_method( self.create_streaming_sequence, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/CreateStreamingSequence", ), - self.get_sequence_report: gapic_v1.method.wrap_method( + self.get_sequence_report: self._wrap_method( self.get_sequence_report, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/GetSequenceReport", ), - self.get_streaming_sequence_report: gapic_v1.method.wrap_method( + self.get_streaming_sequence_report: self._wrap_method( self.get_streaming_sequence_report, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/GetStreamingSequenceReport", ), - self.attempt_sequence: gapic_v1.method.wrap_method( + self.attempt_sequence: self._wrap_method( self.attempt_sequence, default_retry=retries.Retry( initial=0.1, @@ -167,56 +233,68 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/AttemptSequence", ), - self.attempt_streaming_sequence: gapic_v1.method.wrap_method( + self.attempt_streaming_sequence: self._wrap_method( self.attempt_streaming_sequence, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/AttemptStreamingSequence", + is_streaming=True, ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py index 3b0484988bcb..c26a4d8ec34c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -39,6 +43,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import SequenceServiceTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -137,6 +145,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -187,6 +197,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -242,6 +258,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -263,8 +280,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py index bc557fde03c0..12f399d36d9b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -43,6 +46,7 @@ from .base import SequenceServiceTransport, DEFAULT_CLIENT_INFO from .grpc import SequenceServiceGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -184,6 +188,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -235,6 +241,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -289,6 +301,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -310,10 +323,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -494,29 +520,36 @@ def attempt_streaming_sequence(self) -> Callable[ return self._stubs['attempt_streaming_sequence'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.create_sequence: self._wrap_method( + self.create_sequence: self._wrap_async_method( self.create_sequence, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/CreateSequence", ), - self.create_streaming_sequence: self._wrap_method( + self.create_streaming_sequence: self._wrap_async_method( self.create_streaming_sequence, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/CreateStreamingSequence", ), - self.get_sequence_report: self._wrap_method( + self.get_sequence_report: self._wrap_async_method( self.get_sequence_report, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/GetSequenceReport", ), - self.get_streaming_sequence_report: self._wrap_method( + self.get_streaming_sequence_report: self._wrap_async_method( self.get_streaming_sequence_report, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/GetStreamingSequenceReport", ), - self.attempt_sequence: self._wrap_method( + self.attempt_sequence: self._wrap_async_method( self.attempt_sequence, default_retry=retries.AsyncRetry( initial=0.1, @@ -530,64 +563,71 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=10.0, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/AttemptSequence", ), - self.attempt_streaming_sequence: self._wrap_method( + self.attempt_streaming_sequence: self._wrap_async_method( self.attempt_streaming_sequence, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.SequenceService/AttemptStreamingSequence", + is_streaming=True, ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest.py index cf164952e89b..472dfeec16f1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -515,6 +516,7 @@ class SequenceServiceRestStub: _session: AuthorizedSession _host: str _interceptor: SequenceServiceRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class SequenceServiceRestTransport(_BaseSequenceServiceRestTransport): @@ -544,6 +546,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[SequenceServiceRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -583,6 +587,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -594,7 +601,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -615,20 +624,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: sequence.AttemptSequenceRequest, *, @@ -688,7 +712,17 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._AttemptSequence._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = SequenceServiceRestTransport._AttemptSequence._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -707,21 +741,36 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - stream=True, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + stream=True, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: sequence.AttemptStreamingSequenceRequest, *, @@ -788,7 +837,17 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._AttemptStreamingSequence._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = SequenceServiceRestTransport._AttemptStreamingSequence._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -797,7 +856,6 @@ def __call__(self, # Return the response resp = rest_streaming.ResponseIterator(response, sequence.AttemptStreamingSequenceResponse) - resp = self._interceptor.post_attempt_streaming_sequence(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_attempt_streaming_sequence_with_metadata(resp, response_metadata) @@ -829,20 +887,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: gs_sequence.CreateSequenceRequest, *, @@ -909,7 +982,17 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._CreateSequence._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = SequenceServiceRestTransport._CreateSequence._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -921,7 +1004,6 @@ def __call__(self, pb_resp = gs_sequence.Sequence.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_sequence(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_sequence_with_metadata(resp, response_metadata) @@ -958,20 +1040,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: sequence.CreateStreamingSequenceRequest, *, @@ -1039,7 +1136,17 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._CreateStreamingSequence._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = SequenceServiceRestTransport._CreateStreamingSequence._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1051,7 +1158,6 @@ def __call__(self, pb_resp = sequence.StreamingSequence.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_streaming_sequence(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_streaming_sequence_with_metadata(resp, response_metadata) @@ -1088,19 +1194,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: sequence.GetSequenceReportRequest, *, @@ -1165,7 +1286,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._GetSequenceReport._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._GetSequenceReport._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1177,7 +1307,6 @@ def __call__(self, pb_resp = sequence.SequenceReport.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_sequence_report(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_sequence_report_with_metadata(resp, response_metadata) @@ -1214,19 +1343,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: sequence.GetStreamingSequenceReportRequest, *, @@ -1292,7 +1436,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._GetStreamingSequenceReport._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._GetStreamingSequenceReport._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1304,7 +1457,6 @@ def __call__(self, pb_resp = sequence.StreamingSequenceReport.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_streaming_sequence_report(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_streaming_sequence_report_with_metadata(resp, response_metadata) @@ -1335,7 +1487,7 @@ def attempt_sequence(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AttemptSequence(self._session, self._host, self._interceptor) # type: ignore + return self._AttemptSequence(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def attempt_streaming_sequence(self) -> Callable[ @@ -1343,7 +1495,7 @@ def attempt_streaming_sequence(self) -> Callable[ sequence.AttemptStreamingSequenceResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._AttemptStreamingSequence(self._session, self._host, self._interceptor) # type: ignore + return self._AttemptStreamingSequence(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_sequence(self) -> Callable[ @@ -1351,7 +1503,7 @@ def create_sequence(self) -> Callable[ gs_sequence.Sequence]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateSequence(self._session, self._host, self._interceptor) # type: ignore + return self._CreateSequence(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_streaming_sequence(self) -> Callable[ @@ -1359,7 +1511,7 @@ def create_streaming_sequence(self) -> Callable[ sequence.StreamingSequence]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateStreamingSequence(self._session, self._host, self._interceptor) # type: ignore + return self._CreateStreamingSequence(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_sequence_report(self) -> Callable[ @@ -1367,7 +1519,7 @@ def get_sequence_report(self) -> Callable[ sequence.SequenceReport]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetSequenceReport(self._session, self._host, self._interceptor) # type: ignore + return self._GetSequenceReport(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_streaming_sequence_report(self) -> Callable[ @@ -1375,11 +1527,11 @@ def get_streaming_sequence_report(self) -> Callable[ sequence.StreamingSequenceReport]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetStreamingSequenceReport(self._session, self._host, self._interceptor) # type: ignore + return self._GetStreamingSequenceReport(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseSequenceServiceRestTransport._BaseListLocations, SequenceServiceRestStub): def __hash__(self): @@ -1393,19 +1545,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1468,7 +1635,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1502,7 +1678,7 @@ def __call__(self, @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseSequenceServiceRestTransport._BaseGetLocation, SequenceServiceRestStub): def __hash__(self): @@ -1516,19 +1692,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1591,7 +1782,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1625,7 +1825,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseSequenceServiceRestTransport._BaseSetIamPolicy, SequenceServiceRestStub): def __hash__(self): @@ -1639,20 +1839,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -1715,7 +1930,17 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = SequenceServiceRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1749,7 +1974,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseSequenceServiceRestTransport._BaseGetIamPolicy, SequenceServiceRestStub): def __hash__(self): @@ -1763,19 +1988,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -1838,7 +2078,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1872,7 +2121,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseSequenceServiceRestTransport._BaseTestIamPermissions, SequenceServiceRestStub): def __hash__(self): @@ -1886,20 +2135,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -1962,7 +2226,17 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = SequenceServiceRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1996,7 +2270,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseSequenceServiceRestTransport._BaseListOperations, SequenceServiceRestStub): def __hash__(self): @@ -2010,19 +2284,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2085,7 +2374,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2119,7 +2417,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseSequenceServiceRestTransport._BaseGetOperation, SequenceServiceRestStub): def __hash__(self): @@ -2133,19 +2431,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2208,7 +2521,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2242,7 +2564,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseSequenceServiceRestTransport._BaseDeleteOperation, SequenceServiceRestStub): def __hash__(self): @@ -2256,19 +2578,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2328,7 +2665,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2339,7 +2685,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseSequenceServiceRestTransport._BaseCancelOperation, SequenceServiceRestStub): def __hash__(self): @@ -2353,19 +2699,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2425,7 +2786,16 @@ def __call__(self, ) # Send the request - response = SequenceServiceRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = SequenceServiceRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest_base.py index 837e825aa247..eaf7eadf3eff 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -53,6 +54,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -73,6 +76,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -88,7 +94,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseAttemptSequence: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/client.py index 03275a1c5db5..5b374e7544cd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.showcase_v1beta1._compat import _observability from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -503,18 +504,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., TestingTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py index 29e6392c4ed4..1743258a589b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version +from google.showcase_v1beta1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -56,6 +63,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -86,6 +94,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -123,99 +134,167 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.create_session: gapic_v1.method.wrap_method( + self.create_session: self._wrap_method( self.create_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/CreateSession", ), - self.get_session: gapic_v1.method.wrap_method( + self.get_session: self._wrap_method( self.get_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/GetSession", ), - self.list_sessions: gapic_v1.method.wrap_method( + self.list_sessions: self._wrap_method( self.list_sessions, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/ListSessions", ), - self.delete_session: gapic_v1.method.wrap_method( + self.delete_session: self._wrap_method( self.delete_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/DeleteSession", ), - self.report_session: gapic_v1.method.wrap_method( + self.report_session: self._wrap_method( self.report_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/ReportSession", ), - self.list_tests: gapic_v1.method.wrap_method( + self.list_tests: self._wrap_method( self.list_tests, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/ListTests", ), - self.delete_test: gapic_v1.method.wrap_method( + self.delete_test: self._wrap_method( self.delete_test, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/DeleteTest", ), - self.verify_test: gapic_v1.method.wrap_method( + self.verify_test: self._wrap_method( self.verify_test, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/VerifyTest", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: gapic_v1.method.wrap_method( + self.set_iam_policy: self._wrap_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: gapic_v1.method.wrap_method( + self.get_iam_policy: self._wrap_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: gapic_v1.method.wrap_method( + self.test_iam_permissions: self._wrap_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py index 7d28f07c441e..81aa2400356c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -38,6 +42,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import TestingTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -140,6 +148,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -190,6 +200,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -245,6 +261,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -266,8 +283,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py index 5a66af994eb8..b966c83fa9ff 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.showcase_v1beta1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -42,6 +45,7 @@ from .base import TestingTransport, DEFAULT_CLIENT_INFO from .grpc import TestingGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -187,6 +191,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -238,6 +244,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -292,6 +304,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -313,10 +326,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -555,100 +581,115 @@ def verify_test(self) -> Callable[ return self._stubs['verify_test'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.create_session: self._wrap_method( + self.create_session: self._wrap_async_method( self.create_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/CreateSession", ), - self.get_session: self._wrap_method( + self.get_session: self._wrap_async_method( self.get_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/GetSession", ), - self.list_sessions: self._wrap_method( + self.list_sessions: self._wrap_async_method( self.list_sessions, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/ListSessions", ), - self.delete_session: self._wrap_method( + self.delete_session: self._wrap_async_method( self.delete_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/DeleteSession", ), - self.report_session: self._wrap_method( + self.report_session: self._wrap_async_method( self.report_session, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/ReportSession", ), - self.list_tests: self._wrap_method( + self.list_tests: self._wrap_async_method( self.list_tests, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/ListTests", ), - self.delete_test: self._wrap_method( + self.delete_test: self._wrap_async_method( self.delete_test, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/DeleteTest", ), - self.verify_test: self._wrap_method( + self.verify_test: self._wrap_async_method( self.verify_test, default_timeout=None, client_info=client_info, + method_name="google.showcase.v1beta1.Testing/VerifyTest", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.set_iam_policy: self._wrap_method( + self.set_iam_policy: self._wrap_async_method( self.set_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), - self.get_iam_policy: self._wrap_method( + self.get_iam_policy: self._wrap_async_method( self.get_iam_policy, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), - self.test_iam_permissions: self._wrap_method( + self.test_iam_permissions: self._wrap_async_method( self.test_iam_permissions, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest.py index 814d313491e8..7aac1f262a8d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.showcase_v1beta1._compat import transcode_request +from google.showcase_v1beta1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -570,6 +571,7 @@ class TestingRestStub: _session: AuthorizedSession _host: str _interceptor: TestingRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class TestingRestTransport(_BaseTestingRestTransport): @@ -603,6 +605,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[TestingRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -642,6 +646,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -653,7 +660,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -674,20 +683,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.CreateSessionRequest, *, @@ -758,7 +782,17 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._CreateSession._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = TestingRestTransport._CreateSession._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -770,7 +804,6 @@ def __call__(self, pb_resp = testing.Session.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_session(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_session_with_metadata(resp, response_metadata) @@ -807,19 +840,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.DeleteSessionRequest, *, @@ -878,7 +926,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._DeleteSession._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._DeleteSession._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -897,19 +954,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.DeleteTestRequest, *, @@ -968,7 +1040,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._DeleteTest._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._DeleteTest._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -987,19 +1068,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.GetSessionRequest, *, @@ -1070,7 +1166,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._GetSession._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._GetSession._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1082,7 +1187,6 @@ def __call__(self, pb_resp = testing.Session.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_session(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_session_with_metadata(resp, response_metadata) @@ -1119,19 +1223,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.ListSessionsRequest, *, @@ -1195,7 +1314,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._ListSessions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._ListSessions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1207,7 +1335,6 @@ def __call__(self, pb_resp = testing.ListSessionsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_sessions(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_sessions_with_metadata(resp, response_metadata) @@ -1244,19 +1371,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.ListTestsRequest, *, @@ -1321,7 +1463,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._ListTests._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._ListTests._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1333,7 +1484,6 @@ def __call__(self, pb_resp = testing.ListTestsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_tests(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_tests_with_metadata(resp, response_metadata) @@ -1370,19 +1520,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.ReportSessionRequest, *, @@ -1448,7 +1613,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._ReportSession._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._ReportSession._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1460,7 +1634,6 @@ def __call__(self, pb_resp = testing.ReportSessionResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_report_session(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_report_session_with_metadata(resp, response_metadata) @@ -1497,19 +1670,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: testing.VerifyTestRequest, *, @@ -1572,7 +1760,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._VerifyTest._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._VerifyTest._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1584,7 +1781,6 @@ def __call__(self, pb_resp = testing.VerifyTestResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_verify_test(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_verify_test_with_metadata(resp, response_metadata) @@ -1615,7 +1811,7 @@ def create_session(self) -> Callable[ testing.Session]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateSession(self._session, self._host, self._interceptor) # type: ignore + return self._CreateSession(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_session(self) -> Callable[ @@ -1623,7 +1819,7 @@ def delete_session(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteSession(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteSession(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_test(self) -> Callable[ @@ -1631,7 +1827,7 @@ def delete_test(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteTest(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteTest(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_session(self) -> Callable[ @@ -1639,7 +1835,7 @@ def get_session(self) -> Callable[ testing.Session]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetSession(self._session, self._host, self._interceptor) # type: ignore + return self._GetSession(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_sessions(self) -> Callable[ @@ -1647,7 +1843,7 @@ def list_sessions(self) -> Callable[ testing.ListSessionsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListSessions(self._session, self._host, self._interceptor) # type: ignore + return self._ListSessions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_tests(self) -> Callable[ @@ -1655,7 +1851,7 @@ def list_tests(self) -> Callable[ testing.ListTestsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListTests(self._session, self._host, self._interceptor) # type: ignore + return self._ListTests(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def report_session(self) -> Callable[ @@ -1663,7 +1859,7 @@ def report_session(self) -> Callable[ testing.ReportSessionResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ReportSession(self._session, self._host, self._interceptor) # type: ignore + return self._ReportSession(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def verify_test(self) -> Callable[ @@ -1671,11 +1867,11 @@ def verify_test(self) -> Callable[ testing.VerifyTestResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._VerifyTest(self._session, self._host, self._interceptor) # type: ignore + return self._VerifyTest(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseTestingRestTransport._BaseListLocations, TestingRestStub): def __hash__(self): @@ -1689,19 +1885,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1764,7 +1975,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1798,7 +2018,7 @@ def __call__(self, @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseTestingRestTransport._BaseGetLocation, TestingRestStub): def __hash__(self): @@ -1812,19 +2032,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1887,7 +2122,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1921,7 +2165,7 @@ def __call__(self, @property def set_iam_policy(self): - return self._SetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._SetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _SetIamPolicy(_BaseTestingRestTransport._BaseSetIamPolicy, TestingRestStub): def __hash__(self): @@ -1935,20 +2179,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -2011,7 +2270,17 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._SetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = TestingRestTransport._SetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2045,7 +2314,7 @@ def __call__(self, @property def get_iam_policy(self): - return self._GetIamPolicy(self._session, self._host, self._interceptor) # type: ignore + return self._GetIamPolicy(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetIamPolicy(_BaseTestingRestTransport._BaseGetIamPolicy, TestingRestStub): def __hash__(self): @@ -2059,19 +2328,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -2134,7 +2418,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._GetIamPolicy._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._GetIamPolicy._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2168,7 +2461,7 @@ def __call__(self, @property def test_iam_permissions(self): - return self._TestIamPermissions(self._session, self._host, self._interceptor) # type: ignore + return self._TestIamPermissions(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _TestIamPermissions(_BaseTestingRestTransport._BaseTestIamPermissions, TestingRestStub): def __hash__(self): @@ -2182,20 +2475,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -2258,7 +2566,17 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._TestIamPermissions._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = TestingRestTransport._TestIamPermissions._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2292,7 +2610,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseTestingRestTransport._BaseListOperations, TestingRestStub): def __hash__(self): @@ -2306,19 +2624,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2381,7 +2714,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2415,7 +2757,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseTestingRestTransport._BaseGetOperation, TestingRestStub): def __hash__(self): @@ -2429,19 +2771,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2504,7 +2861,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2538,7 +2904,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseTestingRestTransport._BaseDeleteOperation, TestingRestStub): def __hash__(self): @@ -2552,19 +2918,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2624,7 +3005,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2635,7 +3025,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseTestingRestTransport._BaseCancelOperation, TestingRestStub): def __hash__(self): @@ -2649,19 +3039,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2721,7 +3126,16 @@ def __call__(self, ) # Send the request - response = TestingRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = TestingRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest_base.py index e3b29c7866e4..f5592b41873f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -52,6 +53,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -72,6 +75,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -87,7 +93,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseCreateSession: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_compat.py index 70cedabe848e..91993149f485 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_compat.py @@ -25,7 +25,17 @@ import google.auth.transport.mtls from google.showcase_v1beta1._compat import transcode_request -from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.showcase_v1beta1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.showcase_v1beta1._compat import setup_request_id from google.auth.exceptions import MutualTLSChannelError @@ -528,3 +538,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.showcase_v1beta1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.showcase_v1beta1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index a0ff44a080c2..2838d603e7fc 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -63,6 +63,7 @@ from google.showcase_v1beta1.services.echo import EchoClient from google.showcase_v1beta1.services.echo import pagers from google.showcase_v1beta1.services.echo import transports +from google.showcase_v1beta1.services.echo.client import _observability from google.showcase_v1beta1.types import echo as gs_echo import google.api_core.operation_async as operation_async # type: ignore import google.auth @@ -1057,6 +1058,192 @@ def test_echo_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_echo_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.EchoGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = EchoClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_echo_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.EchoGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.EchoGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_echo_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.EchoGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.EchoGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_echo_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.EchoGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_echo_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.EchoGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.EchoGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.EchoGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.EchoGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_echo_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.EchoGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.EchoGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (EchoClient, transports.EchoGrpcTransport, "grpc", grpc_helpers), (EchoAsyncClient, transports.EchoGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index f1aa117884e4..a9d0b5f347a7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -60,6 +60,7 @@ from google.showcase_v1beta1.services.identity import IdentityClient from google.showcase_v1beta1.services.identity import pagers from google.showcase_v1beta1.services.identity import transports +from google.showcase_v1beta1.services.identity.client import _observability from google.showcase_v1beta1.types import identity import google.auth import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore @@ -758,6 +759,192 @@ def test_identity_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_identity_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.IdentityGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = IdentityClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_identity_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.IdentityGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.IdentityGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_identity_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.IdentityGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.IdentityGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_identity_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.IdentityGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_identity_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.IdentityGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.IdentityGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.IdentityGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.IdentityGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_identity_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.IdentityGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.IdentityGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (IdentityClient, transports.IdentityGrpcTransport, "grpc", grpc_helpers), (IdentityAsyncClient, transports.IdentityGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index 24af62ee1c5c..b2a984e65b9d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -63,6 +63,7 @@ from google.showcase_v1beta1.services.messaging import MessagingClient from google.showcase_v1beta1.services.messaging import pagers from google.showcase_v1beta1.services.messaging import transports +from google.showcase_v1beta1.services.messaging.client import _observability from google.showcase_v1beta1.types import messaging import google.api_core.operation_async as operation_async # type: ignore import google.auth @@ -762,6 +763,192 @@ def test_messaging_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_messaging_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.MessagingGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = MessagingClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_messaging_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.MessagingGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.MessagingGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_messaging_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.MessagingGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.MessagingGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_messaging_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.MessagingGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_messaging_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.MessagingGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.MessagingGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.MessagingGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.MessagingGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_messaging_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.MessagingGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.MessagingGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (MessagingClient, transports.MessagingGrpcTransport, "grpc", grpc_helpers), (MessagingAsyncClient, transports.MessagingGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 1ff0d43412d7..278da48d6481 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -67,6 +67,7 @@ from google.showcase_v1beta1.services.resumable_upload_service import ResumableUploadServiceAsyncClient from google.showcase_v1beta1.services.resumable_upload_service import ResumableUploadServiceClient from google.showcase_v1beta1.services.resumable_upload_service import transports +from google.showcase_v1beta1.services.resumable_upload_service.client import _observability from google.showcase_v1beta1.types import resumable_upload import google.auth @@ -763,6 +764,192 @@ def test_resumable_upload_service_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_resumable_upload_service_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.ResumableUploadServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = ResumableUploadServiceClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_resumable_upload_service_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.ResumableUploadServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.ResumableUploadServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_resumable_upload_service_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.ResumableUploadServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.ResumableUploadServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_resumable_upload_service_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.ResumableUploadServiceGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_resumable_upload_service_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.ResumableUploadServiceGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.ResumableUploadServiceGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_resumable_upload_service_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.ResumableUploadServiceGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (ResumableUploadServiceClient, transports.ResumableUploadServiceGrpcTransport, "grpc", grpc_helpers), (ResumableUploadServiceAsyncClient, transports.ResumableUploadServiceGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), @@ -2229,6 +2416,7 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub_sync result1 = await stub(resumable_upload.UploadMediaRequest()) rest_transport = transport._rest_transport assert rest_transport is not None + assert rest_transport.kind == "rest_asyncio" assert isinstance(result1, resumable_transfer.AsyncResumableUploadSession) result2 = await stub(resumable_upload.UploadMediaRequest()) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index f0e7b916d7a9..2876b35164be 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -59,6 +59,7 @@ from google.showcase_v1beta1.services.sequence_service import SequenceServiceAsyncClient from google.showcase_v1beta1.services.sequence_service import SequenceServiceClient from google.showcase_v1beta1.services.sequence_service import transports +from google.showcase_v1beta1.services.sequence_service.client import _observability from google.showcase_v1beta1.types import sequence from google.showcase_v1beta1.types import sequence as gs_sequence import google.auth @@ -759,6 +760,192 @@ def test_sequence_service_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_sequence_service_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.SequenceServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = SequenceServiceClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_sequence_service_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.SequenceServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.SequenceServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_sequence_service_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.SequenceServiceGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.SequenceServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_sequence_service_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.SequenceServiceGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_sequence_service_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.SequenceServiceGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.SequenceServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.SequenceServiceGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.SequenceServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_sequence_service_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.SequenceServiceGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.SequenceServiceGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (SequenceServiceClient, transports.SequenceServiceGrpcTransport, "grpc", grpc_helpers), (SequenceServiceAsyncClient, transports.SequenceServiceGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 98f0c2573ebd..a2fe4054dcd9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -60,6 +60,7 @@ from google.showcase_v1beta1.services.testing import TestingClient from google.showcase_v1beta1.services.testing import pagers from google.showcase_v1beta1.services.testing import transports +from google.showcase_v1beta1.services.testing.client import _observability from google.showcase_v1beta1.types import testing import google.auth @@ -756,6 +757,192 @@ def test_testing_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_testing_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.TestingGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = TestingClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_testing_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.TestingGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.TestingGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_testing_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.TestingGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.TestingGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_testing_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.TestingGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_testing_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.TestingGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.TestingGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.TestingGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.TestingGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_testing_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.TestingGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.TestingGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (TestingClient, transports.TestingGrpcTransport, "grpc", grpc_helpers), (TestingAsyncClient, transports.TestingGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async), diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py index d7096741a7f9..e9202c61ba46 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/_compat.py @@ -20,18 +20,175 @@ import uuid import google.protobuf.message -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Sequence, Tuple from typing import TYPE_CHECKING, Union -if TYPE_CHECKING: # pragma: NO COVER - import proto # type: ignore[import-untyped] - +from google.api_core import gapic_v1 from google.api_core import path_template from google.api_core.universe import EmptyUniverseError from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +try: + import grpc # type: ignore[import-untyped] +except ImportError: # pragma: NO COVER + grpc = None # type: ignore[assignment] + +if TYPE_CHECKING: # pragma: NO COVER + import proto # type: ignore[import-untyped] + +# The _observability module and OpenTelemetry tracing in wrap_method were introduced +# together in google-api-core 2.36.0+. +# On older versions of google-api-core or when type-checking against them, +# mypy may flag attr-defined or assignment errors when fallback to None occurs. +try: + from google.api_core import _observability # type: ignore[attr-defined] +except ImportError: # pragma: NO COVER + _observability = None # type: ignore[assignment] + +# OpenTelemetry method tracing arguments (client_options, method_name, is_streaming, kind) +# were introduced to gapic_v1.method.wrap_method and gapic_v1.method_async.wrap_method +# in google-api-core 2.36.0+ alongside the _observability module. +# Once the repository's minimum constraint for google-api-core is bumped to >= 2.36.0, +# WRAP_METHOD_SUPPORTS_TRACING, the fallback _FallbackTraceContext, and base transport fallbacks +# can be safely removed. +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request + WRAP_METHOD_SUPPORTS_TRACING = True +else: # pragma: NO COVER + WRAP_METHOD_SUPPORTS_TRACING = False + + # Fallback for older versions of google-api-core without HTTP tracing. + class _FallbackTraceContext: + def __enter__(self) -> "_FallbackTraceContext": + return self + + def __exit__(self, *args: Any) -> None: + pass + + def record_response(self, response: Any) -> None: + pass + + record_http_response = record_response + + def record_error(self, exc: BaseException | None) -> None: + pass + + record_http_error = record_error + + def trace_http_request(*args: Any, **kwargs: Any) -> _FallbackTraceContext: + 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. +# In environments running older google-api-core (< 2.29.0) where tracing is also absent, +# passing `kind` will trigger a TypeError unless stripped. +# Once the minimum constraint for google-api-core is bumped to +# >= 2.29.0, this check and the corresponding fallback in base transports can be safely removed. +ASYNC_WRAP_METHOD_SUPPORTS_KIND = WRAP_METHOD_SUPPORTS_TRACING or hasattr( + gapic_v1.method_async, "_DEFAULT_ASYNC_TRANSPORT_KIND" +) + +try: + from google.api_core import grpc_helpers +except ImportError: # pragma: NO COVER + grpc_helpers = None # type: ignore[assignment] + +try: + from google.api_core import grpc_helpers_async +except ImportError: # pragma: NO COVER + grpc_helpers_async = None # type: ignore[assignment] + + +def apply_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """Applies interceptors to a synchronous gRPC channel with fallback for older google-api-core.""" + if grpc_helpers is not None and hasattr(grpc_helpers, "apply_channel_interceptors"): + return grpc_helpers.apply_channel_interceptors(channel, interceptors) # pragma: NO COVER + if not interceptors: # pragma: NO COVER + return channel + for interceptor in reversed(list(interceptors)): # pragma: NO COVER + if callable(interceptor): + channel = interceptor(channel) + elif grpc is not None: + channel = grpc.intercept_channel(channel, interceptor) + return channel # pragma: NO COVER + + +def _fallback_apply_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: # pragma: NO COVER + """Fallback helper to attach interceptors to an existing async gRPC channel. + + Synchronous gRPC channels allow post-creation wrapping via + `grpc.intercept_channel(channel, *interceptors)`. However, asynchronous + channels (`grpc.aio.Channel`) in grpcio are immutable post-creation and + normally require interceptors at initialization. + + When generated client libraries run against older versions of `google-api-core` + (< 2.36.0) where `grpc_helpers_async.apply_channel_interceptors` is not yet + available, this fallback mutates the channel's internal interceptor lists + directly so telemetry and auth interceptors remain operational. + + Args: + channel (Any): The gRPC channel instance (typically a `grpc.aio.Channel` + or mock). + interceptors (Optional[Sequence[Any]]): A sequence of gRPC interceptor + instances or callables to register on the channel. + + Returns: + Any: The channel instance with interceptors registered. + """ + mapping = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_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) + return channel + + +def apply_async_channel_interceptors( + channel: Any, interceptors: Optional[Sequence[Any]] = None +) -> Any: + """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 + + +apply_channel_interceptors_async = apply_async_channel_interceptors + + try: # note: `#type: ignore` is added because the return type for `should_use_client_cert` # is different than that of the fallback implementation below. This will be removed once diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py index ee8cac5e7107..f73edbb23fc5 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.cloud.storagebatchoperations_v1._compat import _observability from google.cloud.storagebatchoperations_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.cloud.storagebatchoperations_v1._compat import setup_request_id from google.api_core import retry as retries @@ -506,18 +507,32 @@ def __init__(self, *, if isinstance(transport, str) or transport is None else cast(Callable[..., StorageBatchOperationsTransport], transport) ) + # When OpenTelemetry tracing is enabled, pass client_options to the transport + # so it can wire tracing interceptors and method spans. + client_options = None + if ( + _observability is not None + and _observability.is_otel_capabilities_enabled(self._client_options) + ): + # NOTE: Coverage tool ignores this line 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. + client_options = self._client_options # pragma: NO COVER + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + **({"client_options": client_options} if client_options is not None else {}), + } + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG): # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py index 1b5920f9153c..bee249c160a9 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/base.py @@ -17,9 +17,16 @@ from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.storagebatchoperations_v1 import gapic_version as package_version +from google.cloud.storagebatchoperations_v1._compat import ( + ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, + WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, +) +_ASYNC_WRAP_METHOD_SUPPORTS_TRACING = _WRAP_METHOD_SUPPORTS_TRACING +_WRAP_METHOD_SUPPORTS_KIND = _WRAP_METHOD_SUPPORTS_TRACING import google.auth # type: ignore import google.api_core +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries @@ -57,6 +64,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, **kwargs, ) -> None: """Instantiate the transport. @@ -87,6 +95,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Save the scopes. @@ -124,16 +135,67 @@ def __init__( host += ':443' self._host = host + self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} @property def host(self): return self._host + def _wrap_method(self, func, *args, **kwargs): + """Wrap an RPC method with common client-level features. + + Applies retry, timeout, metadata, and tracing wrappers to the + underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_async_method(self, func, *args, **kwargs): + """Wrap an async RPC method with common client-level features. + + Applies asynchronous retry, timeout, metadata, and tracing wrappers + to the underlying RPC method callable. If the runtime `google-api-core` + version supports tracing, transport attributes (`client_options` and + `kind`) are injected. Otherwise, tracing-specific arguments are + stripped for backward compatibility with older `google-api-core` + versions. + """ + if _ASYNC_WRAP_METHOD_SUPPORTS_TRACING: # pragma: NO COVER + kwargs["client_options"] = self._client_options + try: + kind = self.kind + except NotImplementedError: # pragma: NO COVER + kind = None + if kind: # pragma: NO COVER + kwargs["kind"] = kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + # The fallback below strips tracing-specific arguments when an older version + # of google-api-core is installed (which does not accept client_options, etc.). + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) # pragma: NO COVER + def _prep_wrapped_messages(self, client_info): - # Precompute the wrapped methods. + """Precompute and cache wrapped methods for RPC dispatch.""" self._wrapped_methods = { - self.list_jobs: gapic_v1.method.wrap_method( + self.list_jobs: self._wrap_method( self.list_jobs, default_retry=retries.Retry( initial=1.0, @@ -146,8 +208,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/ListJobs", ), - self.get_job: gapic_v1.method.wrap_method( + self.get_job: self._wrap_method( self.get_job, default_retry=retries.Retry( initial=1.0, @@ -160,18 +223,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/GetJob", ), - self.create_job: gapic_v1.method.wrap_method( + self.create_job: self._wrap_method( self.create_job, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/CreateJob", ), - self.delete_job: gapic_v1.method.wrap_method( + self.delete_job: self._wrap_method( self.delete_job, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/DeleteJob", ), - self.cancel_job: gapic_v1.method.wrap_method( + self.cancel_job: self._wrap_method( self.cancel_job, default_retry=retries.Retry( initial=1.0, @@ -184,8 +250,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/CancelJob", ), - self.list_bucket_operations: gapic_v1.method.wrap_method( + self.list_bucket_operations: self._wrap_method( self.list_bucket_operations, default_retry=retries.Retry( initial=1.0, @@ -198,8 +265,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/ListBucketOperations", ), - self.get_bucket_operation: gapic_v1.method.wrap_method( + self.get_bucket_operation: self._wrap_method( self.get_bucket_operation, default_retry=retries.Retry( initial=1.0, @@ -212,36 +280,43 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/GetBucketOperation", ), - self.get_location: gapic_v1.method.wrap_method( + self.get_location: self._wrap_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: gapic_v1.method.wrap_method( + self.list_locations: self._wrap_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: gapic_v1.method.wrap_method( + self.cancel_operation: self._wrap_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: gapic_v1.method.wrap_method( + self.delete_operation: self._wrap_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: gapic_v1.method.wrap_method( + self.get_operation: self._wrap_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: gapic_v1.method.wrap_method( + self.list_operations: self._wrap_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py index 1f997d49aabd..16792a55723a 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc.py @@ -18,7 +18,11 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.storagebatchoperations_v1._compat import _observability, apply_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.api_core import gapic_v1 @@ -38,6 +42,10 @@ import google.protobuf.empty_pb2 as empty_pb2 # type: ignore from .base import StorageBatchOperationsTransport, DEFAULT_CLIENT_INFO +if TYPE_CHECKING: # pragma: NO COVER + # ClientInterceptor was added in google-api-core 2.36.0+; ignore attribute-defined for older api-core versions during type checking + from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined] + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -138,6 +146,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union["ClientInterceptor", Callable[[grpc.Channel], grpc.Channel]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -188,6 +198,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -244,6 +260,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -265,8 +282,19 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] + if ( + _observability is not None + and (otel_interceptor := _observability.get_otel_interceptor(self._client_options)) is not None + and otel_interceptor not in channel_interceptors + and not any(getattr(i, "_is_otel_interceptor", None) is True for i in channel_interceptors) + ): + channel_interceptors.append(otel_interceptor) + + self._grpc_channel = apply_channel_interceptors(self._grpc_channel, channel_interceptors) + self._interceptor = _LoggingClientInterceptor() - self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) + self._logged_channel = grpc.intercept_channel(self._grpc_channel, self._interceptor) # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py index 97a7a3213a3c..875d1d1dfb3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import inspect import json import pickle import logging as std_logging import warnings from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING +from google.cloud.storagebatchoperations_v1._compat import _observability, apply_async_channel_interceptors + +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions @@ -42,6 +45,7 @@ from .base import StorageBatchOperationsTransport, DEFAULT_CLIENT_INFO from .grpc import StorageBatchOperationsGrpcTransport + try: from google.api_core import client_logging # type: ignore CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER @@ -185,6 +189,8 @@ def __init__(self, *, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, ) -> None: """Instantiate the transport. @@ -236,6 +242,12 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[Union[aio.ClientInterceptor, Callable[..., aio.ClientInterceptor]]]]): + Additional interceptors (or callables that apply interceptors) to apply to the + gRPC channel. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. Raises: google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport @@ -291,6 +303,7 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, ) if not self._grpc_channel: @@ -312,10 +325,23 @@ def __init__(self, *, ], ) + channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() - self._grpc_channel._unary_unary_interceptors.append(self._interceptor) + channel_interceptors.append(self._interceptor) + + 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. + 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._logged_channel = self._grpc_channel - self._wrap_with_kind = "kind" in inspect.signature(gapic_v1.method_async.wrap_method).parameters # Wrap messages. This must be done after self._logged_channel exists self._prep_wrapped_messages(client_info) @@ -528,9 +554,12 @@ def get_bucket_operation(self) -> Callable[ return self._stubs['get_bucket_operation'] def _prep_wrapped_messages(self, client_info): - """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + """Precompute and cache wrapped methods for async RPC dispatch. + + Overrides the base class method to use asynchronous wrappers and retries. + """ self._wrapped_methods = { - self.list_jobs: self._wrap_method( + self.list_jobs: self._wrap_async_method( self.list_jobs, default_retry=retries.AsyncRetry( initial=1.0, @@ -543,8 +572,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/ListJobs", ), - self.get_job: self._wrap_method( + self.get_job: self._wrap_async_method( self.get_job, default_retry=retries.AsyncRetry( initial=1.0, @@ -557,18 +587,21 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/GetJob", ), - self.create_job: self._wrap_method( + self.create_job: self._wrap_async_method( self.create_job, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/CreateJob", ), - self.delete_job: self._wrap_method( + self.delete_job: self._wrap_async_method( self.delete_job, default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/DeleteJob", ), - self.cancel_job: self._wrap_method( + self.cancel_job: self._wrap_async_method( self.cancel_job, default_retry=retries.AsyncRetry( initial=1.0, @@ -581,8 +614,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/CancelJob", ), - self.list_bucket_operations: self._wrap_method( + self.list_bucket_operations: self._wrap_async_method( self.list_bucket_operations, default_retry=retries.AsyncRetry( initial=1.0, @@ -595,8 +629,9 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/ListBucketOperations", ), - self.get_bucket_operation: self._wrap_method( + self.get_bucket_operation: self._wrap_async_method( self.get_bucket_operation, default_retry=retries.AsyncRetry( initial=1.0, @@ -609,44 +644,46 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.cloud.storagebatchoperations.v1.StorageBatchOperations/GetBucketOperation", ), - self.get_location: self._wrap_method( + self.get_location: self._wrap_async_method( self.get_location, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), - self.list_locations: self._wrap_method( + self.list_locations: self._wrap_async_method( self.list_locations, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/ListLocations", ), - self.cancel_operation: self._wrap_method( + self.cancel_operation: self._wrap_async_method( self.cancel_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), - self.delete_operation: self._wrap_method( + self.delete_operation: self._wrap_async_method( self.delete_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), - self.get_operation: self._wrap_method( + self.get_operation: self._wrap_async_method( self.get_operation, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), - self.list_operations: self._wrap_method( + self.list_operations: self._wrap_async_method( self.list_operations, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), } - def _wrap_method(self, func, *args, **kwargs): - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): return self._logged_channel.close() diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest.py index 9a4373457926..f1f797714c66 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest.py @@ -18,12 +18,13 @@ from google.auth.transport.requests import AuthorizedSession # type: ignore from google.auth import credentials as ga_credentials # type: ignore +from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -from google.cloud.storagebatchoperations_v1._compat import transcode_request +from google.cloud.storagebatchoperations_v1._compat import transcode_request, trace_http_request import google.protobuf from google.protobuf import json_format @@ -495,6 +496,7 @@ class StorageBatchOperationsRestStub: _session: AuthorizedSession _host: str _interceptor: StorageBatchOperationsRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class StorageBatchOperationsRestTransport(_BaseStorageBatchOperationsRestTransport): @@ -526,6 +528,8 @@ def __init__(self, *, url_scheme: str = 'https', interceptor: Optional[StorageBatchOperationsRestInterceptor] = None, api_audience: Optional[str] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -569,6 +573,9 @@ def __init__(self, *, to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. @@ -580,7 +587,9 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, url_scheme=url_scheme, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) self._session = AuthorizedSession( self._credentials, default_host=self.DEFAULT_HOST) @@ -652,20 +661,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.CancelJobRequest, *, @@ -728,7 +752,17 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._CancelJob._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = StorageBatchOperationsRestTransport._CancelJob._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -740,7 +774,6 @@ def __call__(self, pb_resp = storage_batch_operations.CancelJobResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_cancel_job(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_cancel_job_with_metadata(resp, response_metadata) @@ -777,20 +810,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.CreateJobRequest, *, @@ -856,7 +904,17 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._CreateJob._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = StorageBatchOperationsRestTransport._CreateJob._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -866,7 +924,6 @@ def __call__(self, # Return the response resp = operations_pb2.Operation() json_format.Parse(response.content, resp, ignore_unknown_fields=True) - resp = self._interceptor.post_create_job(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_create_job_with_metadata(resp, response_metadata) @@ -903,19 +960,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.DeleteJobRequest, *, @@ -974,7 +1046,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._DeleteJob._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._DeleteJob._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -993,19 +1074,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.GetBucketOperationRequest, *, @@ -1071,7 +1167,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._GetBucketOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._GetBucketOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1083,7 +1188,6 @@ def __call__(self, pb_resp = storage_batch_operations_types.BucketOperation.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_bucket_operation(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_bucket_operation_with_metadata(resp, response_metadata) @@ -1120,19 +1224,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.GetJobRequest, *, @@ -1197,7 +1316,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._GetJob._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._GetJob._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1209,7 +1337,6 @@ def __call__(self, pb_resp = storage_batch_operations_types.Job.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_get_job(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_get_job_with_metadata(resp, response_metadata) @@ -1246,19 +1373,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.ListBucketOperationsRequest, *, @@ -1324,7 +1466,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._ListBucketOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._ListBucketOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1336,7 +1487,6 @@ def __call__(self, pb_resp = storage_batch_operations.ListBucketOperationsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_bucket_operations(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_bucket_operations_with_metadata(resp, response_metadata) @@ -1373,19 +1523,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: storage_batch_operations.ListJobsRequest, *, @@ -1448,7 +1613,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._ListJobs._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._ListJobs._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1460,7 +1634,6 @@ def __call__(self, pb_resp = storage_batch_operations.ListJobsResponse.pb(resp) json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - resp = self._interceptor.post_list_jobs(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] resp, _ = self._interceptor.post_list_jobs_with_metadata(resp, response_metadata) @@ -1491,7 +1664,7 @@ def cancel_job(self) -> Callable[ storage_batch_operations.CancelJobResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CancelJob(self._session, self._host, self._interceptor) # type: ignore + return self._CancelJob(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def create_job(self) -> Callable[ @@ -1499,7 +1672,7 @@ def create_job(self) -> Callable[ operations_pb2.Operation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._CreateJob(self._session, self._host, self._interceptor) # type: ignore + return self._CreateJob(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def delete_job(self) -> Callable[ @@ -1507,7 +1680,7 @@ def delete_job(self) -> Callable[ empty_pb2.Empty]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._DeleteJob(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteJob(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_bucket_operation(self) -> Callable[ @@ -1515,7 +1688,7 @@ def get_bucket_operation(self) -> Callable[ storage_batch_operations_types.BucketOperation]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetBucketOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetBucketOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_job(self) -> Callable[ @@ -1523,7 +1696,7 @@ def get_job(self) -> Callable[ storage_batch_operations_types.Job]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._GetJob(self._session, self._host, self._interceptor) # type: ignore + return self._GetJob(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_bucket_operations(self) -> Callable[ @@ -1531,7 +1704,7 @@ def list_bucket_operations(self) -> Callable[ storage_batch_operations.ListBucketOperationsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListBucketOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListBucketOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def list_jobs(self) -> Callable[ @@ -1539,11 +1712,11 @@ def list_jobs(self) -> Callable[ storage_batch_operations.ListJobsResponse]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast - return self._ListJobs(self._session, self._host, self._interceptor) # type: ignore + return self._ListJobs(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore @property def get_location(self): - return self._GetLocation(self._session, self._host, self._interceptor) # type: ignore + return self._GetLocation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetLocation(_BaseStorageBatchOperationsRestTransport._BaseGetLocation, StorageBatchOperationsRestStub): def __hash__(self): @@ -1557,19 +1730,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1632,7 +1820,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._GetLocation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._GetLocation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1666,7 +1863,7 @@ def __call__(self, @property def list_locations(self): - return self._ListLocations(self._session, self._host, self._interceptor) # type: ignore + return self._ListLocations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListLocations(_BaseStorageBatchOperationsRestTransport._BaseListLocations, StorageBatchOperationsRestStub): def __hash__(self): @@ -1680,19 +1877,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1755,7 +1967,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._ListLocations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._ListLocations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1789,7 +2010,7 @@ def __call__(self, @property def cancel_operation(self): - return self._CancelOperation(self._session, self._host, self._interceptor) # type: ignore + return self._CancelOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _CancelOperation(_BaseStorageBatchOperationsRestTransport._BaseCancelOperation, StorageBatchOperationsRestStub): def __hash__(self): @@ -1803,20 +2024,35 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1876,7 +2112,17 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._CancelOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + response = StorageBatchOperationsRestTransport._CancelOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + body, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1887,7 +2133,7 @@ def __call__(self, @property def delete_operation(self): - return self._DeleteOperation(self._session, self._host, self._interceptor) # type: ignore + return self._DeleteOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _DeleteOperation(_BaseStorageBatchOperationsRestTransport._BaseDeleteOperation, StorageBatchOperationsRestStub): def __hash__(self): @@ -1901,19 +2147,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1973,7 +2234,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._DeleteOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._DeleteOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1984,7 +2254,7 @@ def __call__(self, @property def get_operation(self): - return self._GetOperation(self._session, self._host, self._interceptor) # type: ignore + return self._GetOperation(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _GetOperation(_BaseStorageBatchOperationsRestTransport._BaseGetOperation, StorageBatchOperationsRestStub): def __hash__(self): @@ -1998,19 +2268,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2073,7 +2358,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._GetOperation._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._GetOperation._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2107,7 +2401,7 @@ def __call__(self, @property def list_operations(self): - return self._ListOperations(self._session, self._host, self._interceptor) # type: ignore + return self._ListOperations(self._session, self._host, self._interceptor, getattr(self, "_client_options", None)) # type: ignore class _ListOperations(_BaseStorageBatchOperationsRestTransport._BaseListOperations, StorageBatchOperationsRestStub): def __hash__(self): @@ -2121,19 +2415,34 @@ def _get_response( session, timeout, transcoded_request, - body=None): + body=None, + client_options=None, + url_template=None): + """Execute the HTTP request over the transport session with + OpenTelemetry tracing and metadata propagation.""" uri = transcoded_request['uri'] method = transcoded_request['method'] headers = dict(metadata) headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, + url = "{host}{uri}".format(host=host, uri=uri) + + with trace_http_request( + client_options=client_options, + method=method, + url=url, + url_template=url_template, headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - ) - return response + body=body, + ) as trace_ctx: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + trace_ctx.record_response(response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2196,7 +2505,16 @@ def __call__(self, ) # Send the request - response = StorageBatchOperationsRestTransport._ListOperations._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request) + response = StorageBatchOperationsRestTransport._ListOperations._get_response( + self._host, + metadata, + query_params, + self._session, + timeout, + transcoded_request, + client_options=getattr(self, "_client_options", None), + url_template=http_options[0].get("uri") if http_options else None, + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest_base.py index 3066715fa681..9aa639cef740 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/rest_base.py @@ -16,6 +16,7 @@ import json # type: ignore from google.api_core import path_template from google.api_core import gapic_v1 +from google.api_core.client_options import ClientOptions from google.protobuf import json_format from google.cloud.location import locations_pb2 # type: ignore @@ -51,6 +52,8 @@ def __init__(self, *, always_use_jwt_access: Optional[bool] = False, url_scheme: str = 'https', api_audience: Optional[str] = None, + client_options: Optional[Union[ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. Args: @@ -71,6 +74,9 @@ def __init__(self, *, url_scheme: the protocol scheme for the API endpoint. Normally "https", but for testing or local servers, "http" can be specified. + client_options (Optional[Union[google.api_core.client_options.ClientOptions, dict]]): + Custom options for the client, containing options such as + custom OpenTelemetry tracer providers. """ # Run the base constructor maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) @@ -86,7 +92,9 @@ def __init__(self, *, credentials=credentials, client_info=client_info, always_use_jwt_access=always_use_jwt_access, - api_audience=api_audience + api_audience=api_audience, + client_options=client_options, + **kwargs, ) class _BaseCancelJob: diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py index 94e6e93cf443..a6b0723802c2 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_compat.py @@ -25,7 +25,17 @@ import google.auth.transport.mtls from google.cloud.storagebatchoperations_v1._compat import transcode_request -from google.cloud.storagebatchoperations_v1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables +from google.cloud.storagebatchoperations_v1._compat import ( + get_universe_domain, + get_api_endpoint, + get_default_mtls_endpoint, + should_use_client_cert, + read_environment_variables, + _observability, + trace_http_request, + apply_channel_interceptors, + apply_async_channel_interceptors, +) from google.cloud.storagebatchoperations_v1._compat import setup_request_id from google.auth.exceptions import MutualTLSChannelError @@ -528,3 +538,72 @@ def test_read_environment_variables(): with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "invalid"}): with pytest.raises(MutualTLSChannelError): read_environment_variables() + + +def test_observability_compat(): + # _observability is exposed from _compat + try: + from google.api_core import _observability as core_observability + assert _observability is core_observability + except ImportError: # pragma: NO COVER + assert _observability is None # pragma: NO COVER + + +def test_trace_http_request_compat(): + # trace_http_request is exposed from _compat and callable as context manager + with trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + + +def test_observability_compat_present(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.storagebatchoperations_v1 import _compat + + # Simulate an environment where google.api_core._observability is available + mock_obs = mock.MagicMock() + mock_obs.trace_http_request = mock.MagicMock() + monkeypatch.setitem(sys.modules, "google.api_core._observability", mock_obs) + monkeypatch.setattr(google.api_core, "_observability", mock_obs, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is mock_obs + assert reloaded.trace_http_request is mock_obs.trace_http_request + finally: + monkeypatch.undo() + importlib.reload(_compat) + + +def test_observability_compat_fallback(monkeypatch): + import importlib + import sys + import google.api_core + from google.cloud.storagebatchoperations_v1 import _compat + + # Simulate an environment where google.api_core._observability is not available + monkeypatch.setitem(sys.modules, "google.api_core._observability", None) + monkeypatch.setattr(google.api_core, "_observability", None, raising=False) + reloaded = importlib.reload(_compat) + try: + assert reloaded._observability is None + with reloaded.trace_http_request(method="GET", url="https://example.com") as trace_ctx: + trace_ctx.record_response(None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + + +def test_apply_channel_interceptors_compat(): + # apply_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_channel_interceptors(dummy_channel, None) + assert result is dummy_channel + + +def test_apply_async_channel_interceptors_compat(): + # apply_async_channel_interceptors is exposed from _compat and callable + dummy_channel = mock.Mock() + result = apply_async_channel_interceptors(dummy_channel, None) + assert result is dummy_channel diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index 91d1b992fe18..10bb5ecf13f2 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -58,6 +58,7 @@ from google.cloud.storagebatchoperations_v1.services.storage_batch_operations import StorageBatchOperationsClient from google.cloud.storagebatchoperations_v1.services.storage_batch_operations import pagers from google.cloud.storagebatchoperations_v1.services.storage_batch_operations import transports +from google.cloud.storagebatchoperations_v1.services.storage_batch_operations.client import _observability from google.cloud.storagebatchoperations_v1.types import storage_batch_operations from google.cloud.storagebatchoperations_v1.types import storage_batch_operations_types from google.longrunning import operations_pb2 # type: ignore @@ -760,6 +761,192 @@ def test_storage_batch_operations_client_client_options_from_dict(): ) +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +@pytest.mark.parametrize("capabilities_enabled, expected_options_injected", [ + pytest.param(True, True, id="otel_injection_enabled"), + pytest.param(False, False, id="otel_injection_disabled"), +]) +def test_storage_batch_operations_client_otel_channel_injection(capabilities_enabled, expected_options_injected): + with ( + mock.patch.object( + _observability, + "is_otel_capabilities_enabled", + return_value=capabilities_enabled, + autospec=True, + ) as mock_is_otel_enabled, + mock.patch.object( + transports.StorageBatchOperationsGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = StorageBatchOperationsClient(transport="grpc") + + mock_is_otel_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + if expected_options_injected: + assert called_kwargs.get("client_options") == client._client_options + else: + assert not called_kwargs.get("client_options") + + +def test_storage_batch_operations_grpc_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + transports.StorageBatchOperationsGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + transport = transports.StorageBatchOperationsGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_storage_batch_operations_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + + with ( + mock.patch.object( + _observability, + "get_otel_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_interceptor, + mock.patch.object( + transports.StorageBatchOperationsGrpcTransport, + "create_channel", + return_value=mock_channel, + ), + mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_channel, + create=True, + ) as mock_apply_interceptors, + ): + options = client_options.ClientOptions() + transport = transports.StorageBatchOperationsGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_interceptor.assert_called_once_with(options) + mock_apply_interceptors.assert_called_once_with( + mock_channel, [mock_otel_interceptor] + ) + assert transport.grpc_channel == mock_channel + + +def test_storage_batch_operations_grpc_transport_custom_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_custom_channel = mock.Mock(spec=grpc.Channel) + + with mock.patch.object( + grpc_helpers, + "apply_channel_interceptors", + return_value=mock_custom_channel, + create=True, + ) as mock_apply_interceptors: + transport = transports.StorageBatchOperationsGrpcTransport( + channel=mock_custom_channel, + interceptors=[mock_interceptor], + ) + + mock_apply_interceptors.assert_called_once_with( + mock_custom_channel, [mock_interceptor] + ) + assert transport.grpc_channel == mock_custom_channel + + +def test_storage_batch_operations_grpc_asyncio_transport_channel_interceptors(): + mock_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with mock.patch.object( + transports.StorageBatchOperationsGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel: + transport = transports.StorageBatchOperationsGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + interceptors=[mock_interceptor], + ) + + assert mock_create_channel.call_count == 1 + assert mock_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +@pytest.mark.skipif( + _observability is None, + reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", +) +def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch.object( + _observability, + "get_otel_async_interceptor", + return_value=mock_otel_interceptor, + ) as mock_get_async_interceptor, + mock.patch.object( + transports.StorageBatchOperationsGrpcAsyncIOTransport, + "create_channel", + return_value=mock_channel, + ) as mock_create_channel, + ): + options = client_options.ClientOptions() + transport = transports.StorageBatchOperationsGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + + mock_get_async_interceptor.assert_called_once_with(options) + assert mock_create_channel.call_count == 1 + assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors + assert transport.grpc_channel == mock_channel + + +def test_storage_batch_operations_grpc_asyncio_transport_custom_channel(): + mock_custom_channel = mock.Mock(spec=aio.Channel) + + with mock.patch.object( + transports.StorageBatchOperationsGrpcAsyncIOTransport, + "create_channel", + ) as mock_create_channel: + transport = transports.StorageBatchOperationsGrpcAsyncIOTransport( + channel=mock_custom_channel, + ) + + assert mock_create_channel.call_count == 0 + assert transport.grpc_channel == mock_custom_channel + + @pytest.mark.parametrize("client_class,transport_class,transport_name,grpc_helpers", [ (StorageBatchOperationsClient, transports.StorageBatchOperationsGrpcTransport, "grpc", grpc_helpers), (StorageBatchOperationsAsyncClient, transports.StorageBatchOperationsGrpcAsyncIOTransport, "grpc_asyncio", grpc_helpers_async),