diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ebecd09ac76..ff608bb2a013 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,7 @@ # # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +exclude: '^packages/gapic-generator/tests/integration/goldens/' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 diff --git a/packages/gapic-generator/gapic/schema/mixins.py b/packages/gapic-generator/gapic/schema/mixins.py index d340ec1189ab..793bb4b3ef99 100644 --- a/packages/gapic-generator/gapic/schema/mixins.py +++ b/packages/gapic-generator/gapic/schema/mixins.py @@ -19,50 +19,60 @@ "DeleteOperation", request_type="operations_pb2.DeleteOperationRequest", response_type="None", + rpc_name="google.longrunning.Operations/DeleteOperation", ), "WaitOperation": wrappers.MixinMethod( "WaitOperation", request_type="operations_pb2.WaitOperationRequest", response_type="operations_pb2.Operation", + rpc_name="google.longrunning.Operations/WaitOperation", ), "ListOperations": wrappers.MixinMethod( "ListOperations", request_type="operations_pb2.ListOperationsRequest", response_type="operations_pb2.ListOperationsResponse", + rpc_name="google.longrunning.Operations/ListOperations", ), "CancelOperation": wrappers.MixinMethod( "CancelOperation", request_type="operations_pb2.CancelOperationRequest", response_type="None", + rpc_name="google.longrunning.Operations/CancelOperation", ), "GetOperation": wrappers.MixinMethod( "GetOperation", request_type="operations_pb2.GetOperationRequest", response_type="operations_pb2.Operation", + rpc_name="google.longrunning.Operations/GetOperation", ), "TestIamPermissions": wrappers.MixinMethod( "TestIamPermissions", request_type="iam_policy_pb2.TestIamPermissionsRequest", response_type="iam_policy_pb2.TestIamPermissionsResponse", + rpc_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), "GetIamPolicy": wrappers.MixinMethod( "GetIamPolicy", request_type="iam_policy_pb2.GetIamPolicyRequest", response_type="policy_pb2.Policy", + rpc_name="google.iam.v1.IAMPolicy/GetIamPolicy", ), "SetIamPolicy": wrappers.MixinMethod( "SetIamPolicy", request_type="iam_policy_pb2.SetIamPolicyRequest", response_type="policy_pb2.Policy", + rpc_name="google.iam.v1.IAMPolicy/SetIamPolicy", ), "ListLocations": wrappers.MixinMethod( "ListLocations", request_type="locations_pb2.ListLocationsRequest", response_type="locations_pb2.ListLocationsResponse", + rpc_name="google.cloud.location.Locations/ListLocations", ), "GetLocation": wrappers.MixinMethod( "GetLocation", request_type="locations_pb2.GetLocationRequest", response_type="locations_pb2.Location", + rpc_name="google.cloud.location.Locations/GetLocation", ), } diff --git a/packages/gapic-generator/gapic/schema/wrappers.py b/packages/gapic-generator/gapic/schema/wrappers.py index 9d17b77257c5..e1acba6b3a8d 100644 --- a/packages/gapic-generator/gapic/schema/wrappers.py +++ b/packages/gapic-generator/gapic/schema/wrappers.py @@ -1463,6 +1463,8 @@ class MixinMethod: name: str request_type: str response_type: str + rpc_name: str = "" + @dataclasses.dataclass(frozen=True) 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..17dc303fb8b8 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,14 +5,15 @@ """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 contextlib import os import json {% if has_auto_populated_fields %} @@ -34,9 +35,32 @@ from google.auth.exceptions import MutualTLSChannelError from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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 e39425bb8117..0672c29d99e7 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,39 @@ 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 %} + body=body, + ) as span: + 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 %} + stream=True, + {% endif %} ) - return response + record_http_response(span, response) + return response {% endmacro %} {# rest_call_method_common includes the common code for a rest __call__ method to be @@ -240,13 +249,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 %} @@ -327,6 +347,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() #} {% for method_name in api.mixin_api_methods.keys() %} @@ -337,26 +361,12 @@ def _prep_wrapped_messages(self, client_info): 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() #} } {% endmacro %} -{# TODO: This helper logic to check whether `kind` needs to be configured in wrap_method -can be removed once we require the correct version of the google-api-core dependency to -avoid having a gRPC code path in an async REST call. -See related issue: https://github.com/googleapis/python-api-core/issues/661. -In the meantime, if an older version of the dependency is installed (which has a wrap_method with -no kind parameter), then an async gRPC call will work correctly and async REST transport -will not be available as a transport. -See related issue: https://github.com/googleapis/gapic-generator-python/issues/2119. #} -{% macro wrap_async_method_macro() %} -def _wrap_method(self, func, *args, **kwargs): - {# TODO: Remove `pragma: NO COVER` once https://github.com/googleapis/python-api-core/pull/688 is merged. #} - if self._wrap_with_kind: # pragma: NO COVER - kwargs["kind"] = self.kind - return gapic_v1.method_async.wrap_method(func, *args, **kwargs) -{% endmacro %} {# `create_interceptor_class` generates an Interceptor class for # synchronous and asynchronous rest transports @@ -437,11 +447,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`. """ @@ -487,7 +497,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): @@ -495,7 +505,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, @@ -521,7 +531,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 %} @@ -543,7 +553,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 = { @@ -565,7 +575,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 01407a160d99..bcba26b04b0f 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 @@ -30,7 +30,14 @@ from google.api_core import exceptions as core_exceptions from google.api_core import extended_operation {% endif %} from google.api_core import gapic_v1 -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, +) {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id {% endif %} @@ -314,17 +321,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 @@ -355,21 +362,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, @@ -397,8 +404,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 @@ -415,7 +422,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 @@ -473,7 +480,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, @@ -510,10 +517,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 @@ -531,19 +542,30 @@ 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) + ): + client_options = self._client_options + # 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( @@ -827,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 f0cf1178da69..fefe8ecce750 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 @@ -53,6 +55,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 }}.""" @@ -75,6 +87,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. @@ -105,6 +118,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] = {} @@ -145,17 +161,38 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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( @@ -178,13 +215,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() #} } @@ -370,7 +412,7 @@ class {{ service.name }}Transport(abc.ABC): @property def kind(self) -> str: - raise NotImplementedError() + return "" {% for operations_service in api.get_extended_operations_services(service)|sort(attribute="name") %} @property 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 e906c9d9ea71..782a24d272ef 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 @@ -8,20 +8,27 @@ import json import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] {% if service.has_lro %} from google.api_core import operations_v1 {% endif %} +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} +from {{package_path}}._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore {% filter sort_lines %} @@ -80,7 +87,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}.", @@ -123,6 +130,15 @@ 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. @@ -143,7 +159,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. @@ -173,6 +189,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. + 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 @@ -231,6 +253,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: @@ -252,6 +275,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 7b8a885d227c..1c4535d14783 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 @@ -3,9 +3,7 @@ {% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %} {% 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 @@ -19,6 +17,9 @@ from google.api_core import retry_async as retries {% if service.has_lro %} from google.api_core import operations_v1 {% endif %} +from google.api_core import client_options as client_options_lib +{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} +from {{package_path}}._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -85,7 +86,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}.", @@ -171,6 +172,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -192,7 +196,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. @@ -222,6 +226,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 @@ -279,6 +288,8 @@ 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, + **kwargs, ) if not self._grpc_channel: @@ -300,10 +311,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -451,11 +499,15 @@ 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) }} + def _wrap_method(self, func, *args, **kwargs): + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_method(func, *args, **kwargs) def close(self): return self._logged_channel.close() - + @property def kind(self) -> str: return "grpc_asyncio" @@ -466,4 +518,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 1bc499c068ee..df6b27b89f94 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 @@ -3,6 +3,7 @@ {% block content %} +import contextlib import logging import json # type: ignore @@ -14,7 +15,7 @@ from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 {% 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -36,6 +37,7 @@ import warnings {{ shared_macros.operations_mixin_imports(api, service, opts) }} +from google.api_core import client_options as client_options_lib from .rest_base import _Base{{ service.name }}RestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -69,6 +71,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): @@ -97,6 +100,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. @@ -142,6 +147,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. @@ -153,7 +161,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) @@ -266,7 +276,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) @@ -318,7 +328,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {{method.output.ident}}]: # 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 0f79d6e1ffef..7a4cef28dd64 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 @@ -35,7 +35,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, record_http_response import google.protobuf @@ -51,17 +51,20 @@ from google.iam.v1 import policy_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore {% endif %} +import contextlib import json # type: ignore import dataclasses from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union {{ shared_macros.operations_mixin_imports(api, service, opts) }} +from google.api_core import client_options as client_options_lib from .rest_base import _Base{{ service.name }}RestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +import asyncio import logging try: @@ -92,6 +95,7 @@ class Async{{service.name}}RestStub: _session: AsyncAuthorizedSession _host: str _interceptor: Async{{service.name}}RestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): """Asynchronous REST backend transport for {{ service.name }}. @@ -111,6 +115,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[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -138,6 +144,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[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 super().__init__( @@ -146,7 +155,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 @@ -154,7 +165,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 %} self._operations_client: Optional[operations_v1.AsyncOperationsRestClient] = None @@ -163,7 +173,11 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {{ shared_macros.prep_wrapped_messages_async_method(api, service)|indent(4) }} - {{ shared_macros.wrap_async_method_macro()|indent(4) }} + def _wrap_method(self, func, *args, **kwargs): + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_method(func, *args, **kwargs) {% for method in service.methods.values()|sort(attribute="name") %} class {{ method.name|make_private }}(_Base{{ service.name }}RestTransport._Base{{method.name}}, Async{{service.name}}RestStub): @@ -260,7 +274,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): return resp {% endif %}{# method.void #} - + {% else %} raise NotImplementedError( "Method {{ method.name }} is not available over REST transport" @@ -322,10 +336,10 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): def {{method.transport_safe_name|snake_case}}(self) -> Callable[ [{{method.input.ident}}], {{method.output.ident}}]: - 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 %} - {% 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 %} @@ -336,4 +350,10 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): async def close(self): await self._session.close() + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + {% endblock %} 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 08887c16eb51..1b075b1feed0 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 68e754caf287..3899a93329c4 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 @@ -904,6 +904,194 @@ def test_{{ service.client_name|snake_case }}_client_options_from_dict(): ) +def test_{{ service.client_name|snake_case }}_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.client._observability", + mock_obs, + ), + mock.patch.object( + transports.{{ service.grpc_transport_name }}, "__init__", return_value=None + ) as patched_transport_init, + ): + client = {{ service.client_name }}(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_{{ service.client_name|snake_case }}_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.client._observability", + mock_obs, + ), + mock.patch.object( + transports.{{ service.grpc_transport_name }}, "__init__", return_value=None + ) as patched_transport_init, + ): + client = {{ service.client_name }}(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_{{ service.name|snake_case }}_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -1209,13 +1397,7 @@ def test_{{ service.name|snake_case }}_base_transport(): transport.operations_client {% endif %} - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_{{ service.name|snake_case }}_base_transport_with_credentials_file(): @@ -1246,6 +1428,94 @@ def test_{{ service.name|snake_case }}_base_transport_with_adc(): adc.assert_called_once() +def test_{{ service.name|snake_case }}_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.{{ service.name }}Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_{{ service.name|snake_case }}_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.{{ service.name }}Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join('.') }}.services.{{ service.name|snake_case }}.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_{{ service.name|snake_case }}_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..f2cadd1f4129 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, + record_http_response, +) {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id {% endif %} @@ -531,4 +540,67 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) + {% endblock %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 05c60cbdc9c3..f38588637879 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -1230,7 +1230,7 @@ def test_{{ method_name }}_rest_required_fields(request_type={{ method.input.ide if key == "{{ auto_populated_field|camel_case }}": assert _UUID4_RE.match(value) break - + # Include {{ auto_populated_field|camel_case }} within expected_params with value mock.ANY expected_params = [p for p in expected_params if p[0] != "{{ auto_populated_field|camel_case }}"] expected_params.append( @@ -1670,6 +1670,13 @@ def test_{{ service.name|snake_case }}_{{ transport_name }}_lro_client(): close.assert_not_called() close.assert_called_once() + {% if transport_name == 'rest_asyncio' %} + with mock.patch.object(type(getattr(client.transport, "{{close_session[transport]}}")), "close") as close: + async with client.transport: + close.assert_not_called() + close.assert_called_once() + {% endif %} + {% endmacro %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove / Update this test macro when async rest is GA. #} @@ -1684,7 +1691,13 @@ def test_unsupported_parameter_rest_asyncio(): credentials={{get_credentials(True)}}, transport="rest_asyncio", client_options=options - ) + ) + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError, match="google.api_core.client_options.ClientOptions.quota_project_id") as exc: # type: ignore + client = {{ service.client_name }}( + credentials={{get_credentials(False)}}, + transport="rest_asyncio", + client_options=options + ) {% endmacro %} @@ -1727,7 +1740,7 @@ def test_unsupported_parameter_rest_asyncio(): # TODO(https://github.com/googleapis/gapic-generator-python/issues/2142): Continue migrating the test cases # in macro::run_transport_tests_for_config into here, and then delete that macro in favor of this one. # TODO(https://github.com/googleapis/gapic-generator-python/issues/2153): As a follow up, migrate gRPC test cases -# into `run_transport_tests_for_config` and make any of the rest specific specific macros which are called within more generic. +# into `run_transport_tests_for_config` and make any of the rest specific specific macros which are called within more generic. #} {% macro run_transport_tests_for_config(service, api, transport, is_async) %} {% for method in service.methods.values() %} @@ -1783,7 +1796,7 @@ def test_unsupported_parameter_rest_asyncio(): {% endmacro %} {# initialize_client_with_transport_test adds coverage for transport clients. - # Note: This test case is needed because we aren't unconditionally + # Note: This test case is needed because we aren't unconditionally # generating the not implemented coverage test for every client. #} {% macro initialize_client_with_transport_test(service, transport, is_async) %} @@ -2086,7 +2099,7 @@ def test_initialize_client_w_{{transport_name}}(): assert response.raw_page is response {% endif %} - + {% if method.server_streaming %} {% if is_async %} assert isinstance(response, AsyncIterable) @@ -2096,7 +2109,7 @@ def test_initialize_client_w_{{transport_name}}(): response = next(response) {% endif %} {% endif %} - + # Establish that the response is the type that we expect. {% if method.void %} assert response is None diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index a9724ae3b450..b2da3565f48a 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -18,18 +18,18 @@ # PIP_INDEX_URL=https://pypi.org/simple nox from __future__ import absolute_import -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path + import os +import shutil import sys import tempfile import typing -import nox # type: ignore - +from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from os import path -import shutil +from pathlib import Path +import nox # type: ignore nox.options.error_on_missing_interpreters = True @@ -407,6 +407,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 = [ @@ -439,6 +444,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 = [ @@ -498,7 +508,13 @@ def showcase_pqc( with showcase_library(session, templates=templates, other_opts=other_opts): session.install("pytest", "pytest-asyncio") session.install("--upgrade", "grpcio>=1.83.0", "grpcio-status>=1.83.0") - session.run("py.test", "--quiet", "--tls", *(session.posargs or ["tests/system/test_pqc.py"]), env=env) + session.run( + "py.test", + "--quiet", + "--tls", + *(session.posargs or ["tests/system/test_pqc.py"]), + env=env, + ) def run_showcase_unit_tests(session, fail_under=100, rest_async_io_enabled=False): @@ -508,6 +524,8 @@ def run_showcase_unit_tests(session, fail_under=100, rest_async_io_enabled=False "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/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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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..ac1340303ed8 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -545,18 +552,29 @@ 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) + ): + client_options = self._client_options + # 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..e8df1262a898 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.asset_v1 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 @@ -35,6 +37,16 @@ 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 AssetServiceTransport(abc.ABC): """Abstract transport class for AssetService.""" @@ -55,6 +67,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 +98,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 +138,49 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +194,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 +216,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 +232,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 +254,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 +270,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 +286,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 +301,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", ), } @@ -554,7 +615,7 @@ def get_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..d391013754a4 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.asset_v1._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.asset_v1.types import asset_service @@ -132,6 +138,15 @@ 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 +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 @@ -238,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: @@ -259,6 +281,22 @@ 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) + + 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/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..14cbce18579a 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.asset_v1._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -179,6 +180,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -230,6 +234,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +294,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -306,10 +317,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -1060,11 +1108,13 @@ def _prep_wrapped_messages(self, client_info): 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, 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, @@ -1080,11 +1130,13 @@ 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, 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, @@ -1100,6 +1152,7 @@ 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, @@ -1115,11 +1168,13 @@ 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, 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, @@ -1135,6 +1190,7 @@ 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, @@ -1150,6 +1206,7 @@ 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, @@ -1165,6 +1222,7 @@ 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, @@ -1179,78 +1237,93 @@ 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, 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.asset.v1.AssetService/AnalyzeMove", ), 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..1cec80206b09 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 @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import json # type: ignore @@ -23,7 +24,7 @@ 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -40,6 +41,7 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseAssetServiceRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -1060,6 +1062,7 @@ class AssetServiceRestStub: _session: AuthorizedSession _host: str _interceptor: AssetServiceRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class AssetServiceRestTransport(_BaseAssetServiceRestTransport): @@ -1087,6 +1090,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 +1135,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 +1149,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 +1204,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.AnalyzeIamPolicyRequest, *, @@ -1272,7 +1296,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1284,7 +1316,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 +1352,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.AnalyzeIamPolicyLongrunningRequest, *, @@ -1402,7 +1447,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1412,7 +1466,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 +1502,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.AnalyzeMoveRequest, *, @@ -1527,7 +1594,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1539,7 +1614,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 +1650,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.AnalyzeOrgPoliciesRequest, *, @@ -1654,7 +1742,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1666,7 +1762,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 +1798,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.AnalyzeOrgPolicyGovernedAssetsRequest, *, @@ -1782,7 +1891,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1794,7 +1911,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 +1947,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.AnalyzeOrgPolicyGovernedContainersRequest, *, @@ -1910,7 +2040,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1922,7 +2060,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 +2096,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.BatchGetAssetsHistoryRequest, *, @@ -2034,7 +2185,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2046,7 +2205,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 +2241,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.BatchGetEffectiveIamPoliciesRequest, *, @@ -2162,7 +2334,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2174,7 +2354,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 +2390,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.CreateFeedRequest, *, @@ -2295,7 +2488,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2307,7 +2509,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 +2545,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.CreateSavedQueryRequest, *, @@ -2422,7 +2637,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2434,7 +2658,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 +2694,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.DeleteFeedRequest, *, @@ -2542,7 +2779,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2561,19 +2806,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.DeleteSavedQueryRequest, *, @@ -2632,7 +2891,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2651,20 +2918,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.ExportAssetsRequest, *, @@ -2730,7 +3011,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2740,7 +3030,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 +3066,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.GetFeedRequest, *, @@ -2860,7 +3163,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2872,7 +3183,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 +3219,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.GetSavedQueryRequest, *, @@ -2986,7 +3310,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2998,7 +3330,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 +3366,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.ListAssetsRequest, *, @@ -3110,7 +3455,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3122,7 +3475,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 +3511,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.ListFeedsRequest, *, @@ -3234,7 +3600,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3246,7 +3620,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 +3656,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.ListSavedQueriesRequest, *, @@ -3358,7 +3745,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3370,7 +3765,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 +3801,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.QueryAssetsRequest, *, @@ -3483,7 +3891,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3495,7 +3912,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 +3948,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.SearchAllIamPoliciesRequest, *, @@ -3607,7 +4037,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3619,7 +4057,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 +4093,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.SearchAllResourcesRequest, *, @@ -3731,7 +4182,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3743,7 +4202,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 +4238,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.UpdateFeedRequest, *, @@ -3864,7 +4336,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3876,7 +4357,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 +4393,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: asset_service.UpdateSavedQueryRequest, *, @@ -3991,7 +4485,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4003,7 +4506,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 +4536,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 +4544,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 +4552,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 +4560,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 +4568,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 +4576,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 +4584,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 +4592,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 +4600,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 +4608,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 +4616,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 +4624,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 +4632,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 +4640,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 +4648,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 +4656,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 +4664,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 +4672,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 +4680,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 +4688,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 +4696,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 +4704,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 +4712,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 +4730,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -4303,7 +4819,15 @@ 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), + ) # 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..1e06fe4114a4 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 @@ -760,6 +760,194 @@ def test_asset_service_client_client_options_from_dict(): ) +def test_asset_service_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.asset_v1.services.asset_service.client._observability", + mock_obs, + ), + mock.patch.object( + transports.AssetServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = AssetServiceClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_asset_service_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.asset_v1.services.asset_service.client._observability", + mock_obs, + ), + mock.patch.object( + transports.AssetServiceGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = AssetServiceClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_asset_service_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -17299,13 +17487,7 @@ def test_asset_service_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_asset_service_base_transport_with_credentials_file(): @@ -17335,6 +17517,94 @@ def test_asset_service_base_transport_with_adc(): adc.assert_called_once() +def test_asset_service_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.asset_v1.services.asset_service.transports.AssetServiceTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.AssetServiceTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_asset_service_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.asset_v1.services.asset_service.transports.AssetServiceTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.AssetServiceTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.asset_v1.services.asset_service.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_asset_service_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..3419cb522117 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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..8af747ffbd1f 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -482,18 +489,29 @@ 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) + ): + client_options = self._client_options + # 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..3e3fcd884737 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.iam.credentials_v1 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 @@ -32,6 +34,16 @@ 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 IAMCredentialsTransport(abc.ABC): """Abstract transport class for IAMCredentials.""" @@ -52,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. @@ -82,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. @@ -119,16 +135,37 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +179,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 +195,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 +211,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 +227,7 @@ def _prep_wrapped_messages(self, client_info): ), default_timeout=60.0, client_info=client_info, + method_name="google.iam.credentials.v1.IAMCredentials/SignJwt", ), } @@ -237,7 +278,7 @@ def sign_jwt(self) -> Callable[ @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..6c3cba718e70 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 @@ -17,17 +17,23 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.iam.credentials_v1._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.iam.credentials_v1.types import common @@ -138,6 +144,15 @@ 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 +203,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 +264,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,6 +286,22 @@ 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) + + 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/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..ebd2a99092bb 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,7 +13,6 @@ # 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 @@ -24,6 +23,8 @@ from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries +from google.api_core import client_options as client_options_lib +from google.iam.credentials_v1._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -185,6 +186,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -236,6 +240,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +299,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -311,10 +322,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -453,6 +501,7 @@ 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, @@ -468,6 +517,7 @@ 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, @@ -483,6 +533,7 @@ 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, @@ -498,13 +549,15 @@ 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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..78b7f9e0ea6f 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 @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import json # type: ignore @@ -23,7 +24,7 @@ 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -37,6 +38,7 @@ from google.iam.credentials_v1.types import common +from google.api_core import client_options as client_options_lib from .rest_base import _BaseIAMCredentialsRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -264,6 +266,7 @@ class IAMCredentialsRestStub: _session: AuthorizedSession _host: str _interceptor: IAMCredentialsRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class IAMCredentialsRestTransport(_BaseIAMCredentialsRestTransport): @@ -300,6 +303,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 +348,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 +362,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 +385,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: common.GenerateAccessTokenRequest, *, @@ -451,7 +475,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -463,7 +496,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 +532,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: common.GenerateIdTokenRequest, *, @@ -576,7 +622,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -588,7 +643,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 +679,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: common.SignBlobRequest, *, @@ -701,7 +769,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -713,7 +790,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 +826,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: common.SignJwtRequest, *, @@ -826,7 +916,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -838,7 +937,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 +967,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 +975,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 +983,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 +991,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..47b2409a80b6 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..07e093134741 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 @@ -750,6 +750,194 @@ def test_iam_credentials_client_client_options_from_dict(): ) +def test_iam_credentials_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.iam.credentials_v1.services.iam_credentials.client._observability", + mock_obs, + ), + mock.patch.object( + transports.IAMCredentialsGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = IAMCredentialsClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_iam_credentials_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.iam.credentials_v1.services.iam_credentials.client._observability", + mock_obs, + ), + mock.patch.object( + transports.IAMCredentialsGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = IAMCredentialsClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_iam_credentials_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -3713,13 +3901,7 @@ def test_iam_credentials_base_transport(): with pytest.raises(NotImplementedError): transport.close() - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_iam_credentials_base_transport_with_credentials_file(): @@ -3749,6 +3931,94 @@ def test_iam_credentials_base_transport_with_adc(): adc.assert_called_once() +def test_iam_credentials_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.iam.credentials_v1.services.iam_credentials.transports.IAMCredentialsTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.IAMCredentialsTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_iam_credentials_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.iam.credentials_v1.services.iam_credentials.transports.IAMCredentialsTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.IAMCredentialsTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.iam.credentials_v1.services.iam_credentials.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_iam_credentials_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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..2c91d9504343 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -665,18 +672,29 @@ 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) + ): + client_options = self._client_options + # 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..208e909834d2 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.eventarc_v1 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 @@ -47,6 +49,16 @@ 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 EventarcTransport(abc.ABC): """Abstract transport class for Eventarc.""" @@ -67,6 +79,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 +110,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 +150,323 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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", ), } @@ -834,7 +919,7 @@ def list_locations(self, @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..3d4ae33c369c 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.eventarc_v1._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.eventarc_v1.types import channel @@ -146,6 +152,15 @@ 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 +211,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 +273,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,6 +295,22 @@ 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) + + 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/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..1df394a7135a 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.eventarc_v1._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -193,6 +194,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -244,6 +248,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +308,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -320,10 +331,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -1383,248 +1431,297 @@ def _prep_wrapped_messages(self, client_info): 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListTriggers", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteTrigger", ), 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: self._wrap_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_, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreateChannel", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/GetProvider", ), 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/ListEnrollments", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/DeleteEnrollment", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.cloud.eventarc.v1.Eventarc/CreatePipeline", ), 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: 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: 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: 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: 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: 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: 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: self._wrap_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, 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.iam.v1.IAMPolicy/TestIamPermissions", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/GetOperation", ), self.list_operations: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..b0653f046ff8 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 @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import json # type: ignore @@ -23,7 +24,7 @@ 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -52,6 +53,7 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseEventarcRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -2008,6 +2010,7 @@ class EventarcRestStub: _session: AuthorizedSession _host: str _interceptor: EventarcRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class EventarcRestTransport(_BaseEventarcRestTransport): @@ -2037,6 +2040,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 +2085,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 +2099,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 +2173,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreateChannelRequest, *, @@ -2243,7 +2267,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2253,7 +2286,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 +2322,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreateChannelConnectionRequest, *, @@ -2370,7 +2416,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2380,7 +2435,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 +2471,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreateEnrollmentRequest, *, @@ -2497,7 +2565,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2507,7 +2584,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 +2620,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreateGoogleApiSourceRequest, *, @@ -2624,7 +2714,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2634,7 +2733,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 +2769,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreateMessageBusRequest, *, @@ -2751,7 +2863,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2761,7 +2882,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 +2918,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreatePipelineRequest, *, @@ -2878,7 +3012,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2888,7 +3031,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 +3067,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.CreateTriggerRequest, *, @@ -3005,7 +3161,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3015,7 +3180,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 +3216,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeleteChannelRequest, *, @@ -3131,7 +3309,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3141,7 +3327,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 +3363,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeleteChannelConnectionRequest, *, @@ -3257,7 +3456,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3267,7 +3474,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 +3510,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeleteEnrollmentRequest, *, @@ -3383,7 +3603,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3393,7 +3621,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 +3657,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeleteGoogleApiSourceRequest, *, @@ -3509,7 +3750,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3519,7 +3768,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 +3804,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeleteMessageBusRequest, *, @@ -3635,7 +3897,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3645,7 +3915,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 +3951,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeletePipelineRequest, *, @@ -3761,7 +4044,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3771,7 +4062,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 +4098,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.DeleteTriggerRequest, *, @@ -3887,7 +4191,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3897,7 +4209,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 +4245,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetChannelRequest, *, @@ -4018,7 +4343,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4030,7 +4363,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 +4399,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetChannelConnectionRequest, *, @@ -4150,7 +4496,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4162,7 +4516,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 +4552,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetEnrollmentRequest, *, @@ -4281,7 +4648,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4293,7 +4668,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 +4704,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetGoogleApiSourceRequest, *, @@ -4409,7 +4797,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4421,7 +4817,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 +4853,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetGoogleChannelConfigRequest, *, @@ -4542,7 +4951,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4554,7 +4971,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 +5007,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetMessageBusRequest, *, @@ -4675,7 +5105,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4687,7 +5125,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 +5161,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetPipelineRequest, *, @@ -4802,7 +5253,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4814,7 +5273,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 +5309,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetProviderRequest, *, @@ -4929,7 +5401,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -4941,7 +5421,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 +5457,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.GetTriggerRequest, *, @@ -5056,7 +5549,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5068,7 +5569,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 +5605,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListChannelConnectionsRequest, *, @@ -5183,7 +5697,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5195,7 +5717,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 +5753,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListChannelsRequest, *, @@ -5308,7 +5843,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5320,7 +5863,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 +5899,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListEnrollmentsRequest, *, @@ -5433,7 +5989,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5445,7 +6009,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 +6045,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListGoogleApiSourcesRequest, *, @@ -5560,7 +6137,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5572,7 +6157,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 +6193,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListMessageBusEnrollmentsRequest, *, @@ -5688,7 +6286,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5700,7 +6306,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 +6342,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListMessageBusesRequest, *, @@ -5815,7 +6434,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5827,7 +6454,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 +6490,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListPipelinesRequest, *, @@ -5942,7 +6582,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -5954,7 +6602,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 +6638,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListProvidersRequest, *, @@ -6067,7 +6728,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6079,7 +6748,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 +6784,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.ListTriggersRequest, *, @@ -6192,7 +6874,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6204,7 +6894,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 +6930,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdateChannelRequest, *, @@ -6321,7 +7024,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6331,7 +7043,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 +7079,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdateEnrollmentRequest, *, @@ -6448,7 +7173,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6458,7 +7192,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 +7228,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdateGoogleApiSourceRequest, *, @@ -6575,7 +7322,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6585,7 +7341,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 +7377,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdateGoogleChannelConfigRequest, *, @@ -6708,7 +7477,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6720,7 +7498,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 +7534,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdateMessageBusRequest, *, @@ -6837,7 +7628,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6847,7 +7647,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 +7683,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdatePipelineRequest, *, @@ -6964,7 +7777,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -6974,7 +7796,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 +7832,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: eventarc.UpdateTriggerRequest, *, @@ -7091,7 +7926,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7101,7 +7945,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 +7975,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 +7983,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 +7991,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 +7999,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 +8007,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 +8015,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 +8023,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 +8031,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 +8039,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 +8047,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 +8055,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 +8063,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 +8071,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 +8079,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 +8087,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 +8095,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 +8103,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 +8111,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 +8119,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 +8127,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 +8135,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 +8143,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 +8151,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 +8159,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 +8167,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 +8175,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 +8183,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 +8191,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 +8199,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 +8207,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 +8215,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 +8223,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 +8231,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 +8239,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 +8247,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 +8255,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 +8263,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 +8271,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 +8279,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 +8297,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -7529,7 +8386,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7563,7 +8428,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 +8442,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -7652,7 +8531,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7686,7 +8573,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 +8587,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: iam_policy_pb2.GetIamPolicyRequest, *, @@ -7775,7 +8676,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7809,7 +8718,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 +8732,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: iam_policy_pb2.SetIamPolicyRequest, *, @@ -7899,7 +8822,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -7933,7 +8865,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 +8879,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: iam_policy_pb2.TestIamPermissionsRequest, *, @@ -8023,7 +8969,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8057,7 +9012,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 +9026,34 @@ def _get_response( 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 = 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), - data=body, - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -8144,7 +9113,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8155,7 +9133,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 +9147,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -8241,7 +9233,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8252,7 +9252,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 +9266,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -8341,7 +9355,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -8375,7 +9397,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 +9411,33 @@ def _get_response( 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 = 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), - ) - return response + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + ) + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -8464,7 +9500,15 @@ 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), + ) # 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..d2e3b45f4bef 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..b155f3c7b4b7 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 @@ -781,6 +781,194 @@ def test_eventarc_client_client_options_from_dict(): ) +def test_eventarc_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.eventarc_v1.services.eventarc.client._observability", + mock_obs, + ), + mock.patch.object( + transports.EventarcGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = EventarcClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_eventarc_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.eventarc_v1.services.eventarc.client._observability", + mock_obs, + ), + mock.patch.object( + transports.EventarcGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = EventarcClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_eventarc_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -30740,13 +30928,7 @@ def test_eventarc_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_eventarc_base_transport_with_credentials_file(): @@ -30776,6 +30958,94 @@ def test_eventarc_base_transport_with_adc(): adc.assert_called_once() +def test_eventarc_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.eventarc_v1.services.eventarc.transports.EventarcTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.EventarcTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_eventarc_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.eventarc_v1.services.eventarc.transports.EventarcTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.EventarcTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.eventarc_v1.services.eventarc.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_eventarc_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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..798a0425a87d 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -538,18 +545,29 @@ 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) + ): + client_options = self._client_options + # 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..c20d78392041 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 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 @@ -35,6 +37,16 @@ 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 ConfigServiceV2Transport(abc.ABC): """Abstract transport class for ConfigServiceV2.""" @@ -58,6 +70,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 +101,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 +141,115 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +264,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 +281,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 +304,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 +321,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 +362,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 +379,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 +408,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", ), } @@ -719,7 +791,7 @@ def cancel_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..72d49407f595 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.logging_v2.types import logging_config @@ -132,6 +138,15 @@ 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 +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 @@ -238,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: @@ -259,6 +281,22 @@ 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) + + 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/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..a5ce74db378b 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.logging_v2._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -179,6 +180,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -230,6 +234,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +294,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -306,10 +317,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -1279,66 +1327,79 @@ def _prep_wrapped_messages(self, client_info): 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucketAsync", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteBucket", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetView", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteView", ), self.list_sinks: self._wrap_method( self.list_sinks, @@ -1355,6 +1416,7 @@ 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, @@ -1371,11 +1433,13 @@ 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, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateSink", ), self.update_sink: self._wrap_method( self.update_sink, @@ -1392,6 +1456,7 @@ 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, @@ -1408,26 +1473,31 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateLink", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetLink", ), self.list_exclusions: self._wrap_method( self.list_exclusions, @@ -1444,6 +1514,7 @@ 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, @@ -1460,16 +1531,19 @@ 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, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateExclusion", ), 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: self._wrap_method( self.delete_exclusion, @@ -1486,53 +1560,63 @@ 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, 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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateCmekSettings", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CopyLogEntries", ), 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: self._wrap_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, 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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..a87c97c94299 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -469,18 +476,29 @@ 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) + ): + client_options = self._client_options + # 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..680bca892842 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 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 @@ -34,6 +36,16 @@ 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 LoggingServiceV2Transport(abc.ABC): """Abstract transport class for LoggingServiceV2.""" @@ -58,6 +70,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 +101,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 +141,37 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +186,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 +203,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 +220,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 +237,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 +254,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 +271,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", ), } @@ -339,7 +386,7 @@ def cancel_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..dcd3cd23305e 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 @@ -17,17 +17,23 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.logging_v2.types import logging @@ -131,6 +137,15 @@ 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 +196,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 +257,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,6 +279,22 @@ 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) + + 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/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..706ec5ff573c 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,7 +13,6 @@ # 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 @@ -24,6 +23,8 @@ from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries +from google.api_core import client_options as client_options_lib +from google.cloud.logging_v2._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -178,6 +179,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -229,6 +233,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +292,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -304,10 +315,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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,6 +562,7 @@ 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, @@ -530,6 +579,7 @@ 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, @@ -546,6 +596,7 @@ 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, @@ -562,6 +613,7 @@ 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, @@ -578,6 +630,7 @@ 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, @@ -594,28 +647,34 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), 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: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..0cde89ca1291 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -470,18 +477,29 @@ 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) + ): + client_options = self._client_options + # 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..da0ff0712968 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 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 @@ -34,6 +36,16 @@ 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 MetricsServiceV2Transport(abc.ABC): """Abstract transport class for MetricsServiceV2.""" @@ -58,6 +70,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 +101,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 +141,37 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +186,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 +203,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 +226,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 +243,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", ), } @@ -303,7 +348,7 @@ def cancel_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..9ed0a1211ea0 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 @@ -17,17 +17,23 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.logging_v2.types import logging_metrics @@ -131,6 +137,15 @@ 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 +196,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 +257,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,6 +279,22 @@ 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) + + 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/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..2a4809875de7 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,7 +13,6 @@ # 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 @@ -24,6 +23,8 @@ from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries +from google.api_core import client_options as client_options_lib +from google.cloud.logging_v2._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -178,6 +179,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -229,6 +233,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +292,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -304,10 +315,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -469,6 +517,7 @@ 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, @@ -485,11 +534,13 @@ 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, 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, @@ -506,6 +557,7 @@ 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, @@ -522,28 +574,33 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), 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: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..fd43efb178b6 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..6f182792654a 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 @@ -736,6 +736,194 @@ def test_config_service_v2_client_client_options_from_dict(): ) +def test_config_service_v2_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = ConfigServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_config_service_v2_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = ConfigServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_config_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -12654,13 +12842,7 @@ def test_config_service_v2_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_config_service_v2_base_transport_with_credentials_file(): @@ -12693,6 +12875,94 @@ def test_config_service_v2_base_transport_with_adc(): adc.assert_called_once() +def test_config_service_v2_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.config_service_v2.transports.ConfigServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.ConfigServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_config_service_v2_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.config_service_v2.transports.ConfigServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.ConfigServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_config_service_v2_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..5a0c4414fb7d 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 @@ -737,6 +737,194 @@ def test_logging_service_v2_client_client_options_from_dict(): ) +def test_logging_service_v2_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = LoggingServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_logging_service_v2_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = LoggingServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -3245,13 +3433,7 @@ def test_logging_service_v2_base_transport(): with pytest.raises(NotImplementedError): transport.close() - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_logging_service_v2_base_transport_with_credentials_file(): @@ -3285,6 +3467,94 @@ def test_logging_service_v2_base_transport_with_adc(): adc.assert_called_once() +def test_logging_service_v2_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.logging_service_v2.transports.LoggingServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.LoggingServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_logging_service_v2_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.logging_service_v2.transports.LoggingServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.LoggingServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_logging_service_v2_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..34109a10343c 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 @@ -735,6 +735,194 @@ def test_metrics_service_v2_client_client_options_from_dict(): ) +def test_metrics_service_v2_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = MetricsServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_metrics_service_v2_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = MetricsServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -3045,13 +3233,7 @@ def test_metrics_service_v2_base_transport(): with pytest.raises(NotImplementedError): transport.close() - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_metrics_service_v2_base_transport_with_credentials_file(): @@ -3085,6 +3267,94 @@ def test_metrics_service_v2_base_transport_with_adc(): adc.assert_called_once() +def test_metrics_service_v2_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.metrics_service_v2.transports.MetricsServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.MetricsServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_metrics_service_v2_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.metrics_service_v2.transports.MetricsServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.MetricsServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_metrics_service_v2_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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..649a3c1ad9a1 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -538,18 +545,29 @@ 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) + ): + client_options = self._client_options + # 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..c20d78392041 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 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 @@ -35,6 +37,16 @@ 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 ConfigServiceV2Transport(abc.ABC): """Abstract transport class for ConfigServiceV2.""" @@ -58,6 +70,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 +101,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 +141,115 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +264,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 +281,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 +304,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 +321,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 +362,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 +379,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 +408,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", ), } @@ -719,7 +791,7 @@ def cancel_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..72d49407f595 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.logging_v2.types import logging_config @@ -132,6 +138,15 @@ 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 +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 @@ -238,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: @@ -259,6 +281,22 @@ 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) + + 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/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..a5ce74db378b 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.logging_v2._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -179,6 +180,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -230,6 +234,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +294,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -306,10 +317,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -1279,66 +1327,79 @@ def _prep_wrapped_messages(self, client_info): 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateBucketAsync", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteBucket", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetView", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/DeleteView", ), self.list_sinks: self._wrap_method( self.list_sinks, @@ -1355,6 +1416,7 @@ 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, @@ -1371,11 +1433,13 @@ 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, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateSink", ), self.update_sink: self._wrap_method( self.update_sink, @@ -1392,6 +1456,7 @@ 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, @@ -1408,26 +1473,31 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateLink", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/GetLink", ), self.list_exclusions: self._wrap_method( self.list_exclusions, @@ -1444,6 +1514,7 @@ 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, @@ -1460,16 +1531,19 @@ 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, default_timeout=120.0, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CreateExclusion", ), 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: self._wrap_method( self.delete_exclusion, @@ -1486,53 +1560,63 @@ 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, 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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/UpdateCmekSettings", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.logging.v2.ConfigServiceV2/CopyLogEntries", ), 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: self._wrap_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, 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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..a87c97c94299 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -469,18 +476,29 @@ 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) + ): + client_options = self._client_options + # 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..680bca892842 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 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 @@ -34,6 +36,16 @@ 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 LoggingServiceV2Transport(abc.ABC): """Abstract transport class for LoggingServiceV2.""" @@ -58,6 +70,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 +101,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 +141,37 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +186,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 +203,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 +220,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 +237,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 +254,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 +271,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", ), } @@ -339,7 +386,7 @@ def cancel_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..dcd3cd23305e 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 @@ -17,17 +17,23 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.logging_v2.types import logging @@ -131,6 +137,15 @@ 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 +196,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 +257,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,6 +279,22 @@ 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) + + 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/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..706ec5ff573c 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,7 +13,6 @@ # 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 @@ -24,6 +23,8 @@ from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries +from google.api_core import client_options as client_options_lib +from google.cloud.logging_v2._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -178,6 +179,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -229,6 +233,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +292,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -304,10 +315,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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,6 +562,7 @@ 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, @@ -530,6 +579,7 @@ 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, @@ -546,6 +596,7 @@ 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, @@ -562,6 +613,7 @@ 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, @@ -578,6 +630,7 @@ 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, @@ -594,28 +647,34 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), 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: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..6f2263b19be2 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -470,18 +477,29 @@ 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) + ): + client_options = self._client_options + # 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..da0ff0712968 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 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 @@ -34,6 +36,16 @@ 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 MetricsServiceV2Transport(abc.ABC): """Abstract transport class for MetricsServiceV2.""" @@ -58,6 +70,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 +101,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 +141,37 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +186,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 +203,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 +226,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 +243,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", ), } @@ -303,7 +348,7 @@ def cancel_operation( @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..9ed0a1211ea0 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 @@ -17,17 +17,23 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.logging_v2._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.logging_v2.types import logging_metrics @@ -131,6 +137,15 @@ 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 +196,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 +257,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,6 +279,22 @@ 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) + + 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/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..2a4809875de7 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,7 +13,6 @@ # 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 @@ -24,6 +23,8 @@ from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries +from google.api_core import client_options as client_options_lib +from google.cloud.logging_v2._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -178,6 +179,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -229,6 +233,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +292,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -304,10 +315,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -469,6 +517,7 @@ 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, @@ -485,11 +534,13 @@ 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, 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, @@ -506,6 +557,7 @@ 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, @@ -522,28 +574,33 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), 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: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..fd43efb178b6 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..6d5c82308996 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 @@ -736,6 +736,194 @@ def test_base_config_service_v2_client_client_options_from_dict(): ) +def test_base_config_service_v2_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = BaseConfigServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_base_config_service_v2_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.ConfigServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = BaseConfigServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_config_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -12654,13 +12842,7 @@ def test_config_service_v2_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_config_service_v2_base_transport_with_credentials_file(): @@ -12693,6 +12875,94 @@ def test_config_service_v2_base_transport_with_adc(): adc.assert_called_once() +def test_config_service_v2_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.config_service_v2.transports.ConfigServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.ConfigServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_config_service_v2_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.config_service_v2.transports.ConfigServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.ConfigServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.config_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_config_service_v2_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..5a0c4414fb7d 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 @@ -737,6 +737,194 @@ def test_logging_service_v2_client_client_options_from_dict(): ) +def test_logging_service_v2_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = LoggingServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_logging_service_v2_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.LoggingServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = LoggingServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -3245,13 +3433,7 @@ def test_logging_service_v2_base_transport(): with pytest.raises(NotImplementedError): transport.close() - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_logging_service_v2_base_transport_with_credentials_file(): @@ -3285,6 +3467,94 @@ def test_logging_service_v2_base_transport_with_adc(): adc.assert_called_once() +def test_logging_service_v2_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.logging_service_v2.transports.LoggingServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.LoggingServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_logging_service_v2_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.logging_service_v2.transports.LoggingServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.LoggingServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.logging_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_logging_service_v2_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..a357936bedee 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 @@ -735,6 +735,194 @@ def test_base_metrics_service_v2_client_client_options_from_dict(): ) +def test_base_metrics_service_v2_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = BaseMetricsServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_base_metrics_service_v2_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.client._observability", + mock_obs, + ), + mock.patch.object( + transports.MetricsServiceV2GrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = BaseMetricsServiceV2Client(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -3045,13 +3233,7 @@ def test_metrics_service_v2_base_transport(): with pytest.raises(NotImplementedError): transport.close() - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_metrics_service_v2_base_transport_with_credentials_file(): @@ -3085,6 +3267,94 @@ def test_metrics_service_v2_base_transport_with_adc(): adc.assert_called_once() +def test_metrics_service_v2_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.metrics_service_v2.transports.MetricsServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.MetricsServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_metrics_service_v2_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.logging_v2.services.metrics_service_v2.transports.MetricsServiceV2Transport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.MetricsServiceV2Transport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.logging_v2.services.metrics_service_v2.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_metrics_service_v2_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: 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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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 7b2e7759cd73..24b3a1cc61d6 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -520,10 +527,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 +543,29 @@ 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) + ): + client_options = self._client_options + # 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..2eb7d3bc1f1b 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 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 @@ -35,6 +37,16 @@ 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 CloudRedisTransport(abc.ABC): """Abstract transport class for CloudRedis.""" @@ -55,6 +67,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 +98,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 +138,143 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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", ), } @@ -399,7 +454,7 @@ def list_locations(self, @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..c50991209393 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.redis_v1._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.location import locations_pb2 # type: ignore @@ -152,6 +158,15 @@ 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 +217,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 +279,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,6 +301,22 @@ 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) + + 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/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..6e58c5dfcc89 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.redis_v1._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -199,6 +200,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -250,6 +254,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +314,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -326,10 +337,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -700,98 +748,117 @@ def _prep_wrapped_messages(self, client_info): 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/RescheduleMaintenance", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), self.wait_operation: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..c8715b256a1f 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 @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import json # type: ignore @@ -23,7 +24,7 @@ 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -40,6 +41,7 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseCloudRedisRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -722,6 +724,7 @@ class CloudRedisRestStub: _session: AuthorizedSession _host: str _interceptor: CloudRedisRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class CloudRedisRestTransport(_BaseCloudRedisRestTransport): @@ -769,6 +772,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 +817,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 +831,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 +911,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -981,7 +1005,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -991,7 +1024,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 +1060,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -1107,7 +1153,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1117,7 +1171,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 +1207,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.ExportInstanceRequest, *, @@ -1234,7 +1301,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1244,7 +1320,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 +1356,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.FailoverInstanceRequest, *, @@ -1361,7 +1450,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1371,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_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 +1505,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -1484,7 +1595,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1496,7 +1615,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 +1651,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.GetInstanceAuthStringRequest, *, @@ -1609,7 +1741,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1621,7 +1761,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 +1797,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.ImportInstanceRequest, *, @@ -1738,7 +1891,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1748,7 +1910,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 +1946,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1863,7 +2038,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1875,7 +2058,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 +2094,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.RescheduleMaintenanceRequest, *, @@ -1992,7 +2188,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2002,7 +2207,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 +2243,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -2119,7 +2337,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2129,7 +2356,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 +2392,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.UpgradeInstanceRequest, *, @@ -2246,7 +2486,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2256,7 +2505,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 +2535,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 +2543,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 +2551,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 +2559,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 +2567,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 +2575,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 +2583,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 +2591,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 +2599,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 +2607,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 +2615,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 +2633,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -2460,7 +2722,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2494,7 +2764,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 +2778,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -2583,7 +2867,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2617,7 +2909,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 +2923,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2703,7 +3009,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2714,7 +3028,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 +3042,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2800,7 +3128,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2811,7 +3147,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 +3161,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2900,7 +3250,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2934,7 +3292,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 +3306,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -3023,7 +3395,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3057,7 +3437,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 +3451,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -3147,7 +3541,16 @@ 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), + ) # 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 4d629a5a8443..77b9ff3d9a27 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 @@ -32,7 +32,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.cloud.redis_v1._compat import transcode_request +from google.cloud.redis_v1._compat import transcode_request, trace_http_request, record_http_response import google.protobuf @@ -40,6 +40,7 @@ from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore +import contextlib import json # type: ignore import dataclasses from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union @@ -49,11 +50,13 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseCloudRedisRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +import asyncio import logging try: @@ -735,6 +738,7 @@ class AsyncCloudRedisRestStub: _session: AsyncAuthorizedSession _host: str _interceptor: AsyncCloudRedisRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): """Asynchronous REST backend transport for CloudRedis. @@ -774,6 +778,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[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -799,6 +805,9 @@ def __init__(self, "http" can be specified. interceptor (Optional[AsyncCloudRedisRestInterceptor]): Interceptor used to manipulate requests, request metadata, and responses. + 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 super().__init__( @@ -807,11 +816,12 @@ 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 @@ -822,98 +832,117 @@ def _prep_wrapped_messages(self, client_info): 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.redis.v1.CloudRedis/RescheduleMaintenance", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/CancelOperation", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/ListOperations", ), self.wait_operation: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_method(func, *args, **kwargs) class _CreateInstance(_BaseCloudRedisRestTransport._BaseCreateInstance, AsyncCloudRedisRestStub): def __hash__(self): @@ -927,20 +956,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -1007,7 +1050,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1060,19 +1112,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -1139,7 +1205,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1192,20 +1266,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.ExportInstanceRequest, *, @@ -1272,7 +1360,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1325,20 +1422,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.FailoverInstanceRequest, *, @@ -1405,7 +1516,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1458,19 +1578,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -1534,7 +1668,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1587,19 +1729,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.GetInstanceAuthStringRequest, *, @@ -1663,7 +1819,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1716,20 +1880,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.ImportInstanceRequest, *, @@ -1796,7 +1974,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1849,19 +2036,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1927,7 +2128,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1980,20 +2189,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.RescheduleMaintenanceRequest, *, @@ -2060,7 +2283,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2113,20 +2345,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -2193,7 +2439,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2246,20 +2501,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.UpgradeInstanceRequest, *, @@ -2326,7 +2595,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2427,71 +2705,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): @@ -2505,19 +2783,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -2580,7 +2872,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2618,7 +2918,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): @@ -2632,19 +2932,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -2707,7 +3021,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2745,7 +3067,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): @@ -2759,19 +3081,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -2831,7 +3167,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2846,7 +3190,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): @@ -2860,19 +3204,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -2932,7 +3290,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2947,7 +3313,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): @@ -2961,19 +3327,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -3036,7 +3416,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3074,7 +3462,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): @@ -3088,19 +3476,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -3163,7 +3565,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3201,7 +3611,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): @@ -3215,20 +3625,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -3291,7 +3715,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -3333,3 +3766,9 @@ def kind(self) -> str: async def close(self): await self._session.close() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() 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..8f4be2d70d8d 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 @@ -768,6 +768,194 @@ def test_cloud_redis_client_client_options_from_dict(): ) +def test_cloud_redis_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.client._observability", + mock_obs, + ), + mock.patch.object( + transports.CloudRedisGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = CloudRedisClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_cloud_redis_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.client._observability", + mock_obs, + ), + mock.patch.object( + transports.CloudRedisGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = CloudRedisClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_cloud_redis_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -11255,7 +11443,13 @@ def test_unsupported_parameter_rest_asyncio(): credentials=async_anonymous_credentials(), transport="rest_asyncio", client_options=options - ) + ) + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError, match="google.api_core.client_options.ClientOptions.quota_project_id") as exc: # type: ignore + client = CloudRedisClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest_asyncio", + client_options=options + ) def test_transport_grpc_default(): @@ -11319,13 +11513,7 @@ def test_cloud_redis_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_cloud_redis_base_transport_with_credentials_file(): @@ -11355,6 +11543,94 @@ def test_cloud_redis_base_transport_with_adc(): adc.assert_called_once() +def test_cloud_redis_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.redis_v1.services.cloud_redis.transports.CloudRedisTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CloudRedisTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_cloud_redis_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.redis_v1.services.cloud_redis.transports.CloudRedisTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CloudRedisTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_cloud_redis_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: @@ -13070,6 +13346,11 @@ async def test_transport_close_rest_asyncio(): close.assert_not_called() close.assert_called_once() + with mock.patch.object(type(getattr(client.transport, "_session")), "close") as close: + async with client.transport: + close.assert_not_called() + close.assert_called_once() + def test_client_ctx(): transports = [ 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..9abfdbf59c1a 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..f235d8cb2c9f 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json @@ -26,6 +27,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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 771b0baa9989..0d60c5042551 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,7 +27,14 @@ 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 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, +) from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport import mtls # type: ignore @@ -520,10 +527,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 +543,29 @@ 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) + ): + client_options = self._client_options + # 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..6de9183a238b 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 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 @@ -35,6 +37,16 @@ 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 CloudRedisTransport(abc.ABC): """Abstract transport class for CloudRedis.""" @@ -55,6 +67,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 +98,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 +138,107 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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", ), } @@ -315,7 +364,7 @@ def list_locations(self, @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..6fafd12d3e6c 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.redis_v1._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.location import locations_pb2 # type: ignore @@ -152,6 +158,15 @@ 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 +217,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 +279,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,6 +301,22 @@ 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) + + 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/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..b748aa41f8ba 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.redis_v1._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -199,6 +200,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -250,6 +254,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +314,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -326,10 +337,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -524,68 +572,81 @@ def _prep_wrapped_messages(self, client_info): 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, 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, 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), 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: self._wrap_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, 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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..03d4ee554380 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 @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import json # type: ignore @@ -23,7 +24,7 @@ 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -40,6 +41,7 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseCloudRedisRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -458,6 +460,7 @@ class CloudRedisRestStub: _session: AuthorizedSession _host: str _interceptor: CloudRedisRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class CloudRedisRestTransport(_BaseCloudRedisRestTransport): @@ -505,6 +508,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 +553,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 +567,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 +647,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -717,7 +741,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -727,7 +760,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 +796,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -843,7 +889,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -853,7 +907,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 +943,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -966,7 +1033,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -978,7 +1053,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 +1089,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1093,7 +1181,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1105,7 +1201,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 +1237,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -1222,7 +1331,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1232,7 +1350,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 +1380,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 +1388,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 +1396,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 +1404,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 +1412,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 +1430,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1388,7 +1519,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1422,7 +1561,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 +1575,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1511,7 +1664,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1545,7 +1706,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 +1720,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1631,7 +1806,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1642,7 +1825,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 +1839,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1728,7 +1925,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1739,7 +1944,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 +1958,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -1828,7 +2047,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1862,7 +2089,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 +2103,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -1951,7 +2192,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1985,7 +2234,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 +2248,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -2075,7 +2338,16 @@ 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), + ) # 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 960d9639a214..1bf9c5b61d2e 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 @@ -32,7 +32,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.cloud.redis_v1._compat import transcode_request +from google.cloud.redis_v1._compat import transcode_request, trace_http_request, record_http_response import google.protobuf @@ -40,6 +40,7 @@ from google.api_core import operations_v1 from google.cloud.location import locations_pb2 # type: ignore +import contextlib import json # type: ignore import dataclasses from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union @@ -49,11 +50,13 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseCloudRedisRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +import asyncio import logging try: @@ -471,6 +474,7 @@ class AsyncCloudRedisRestStub: _session: AsyncAuthorizedSession _host: str _interceptor: AsyncCloudRedisRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): """Asynchronous REST backend transport for CloudRedis. @@ -510,6 +514,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[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -535,6 +541,9 @@ def __init__(self, "http" can be specified. interceptor (Optional[AsyncCloudRedisRestInterceptor]): Interceptor used to manipulate requests, request metadata, and responses. + 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 super().__init__( @@ -543,11 +552,12 @@ 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 @@ -558,68 +568,81 @@ def _prep_wrapped_messages(self, client_info): 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, 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, 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, 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, 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), 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: self._wrap_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, 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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_method(func, *args, **kwargs) class _CreateInstance(_BaseCloudRedisRestTransport._BaseCreateInstance, AsyncCloudRedisRestStub): def __hash__(self): @@ -633,20 +656,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.CreateInstanceRequest, *, @@ -713,7 +750,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -766,19 +812,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.DeleteInstanceRequest, *, @@ -845,7 +905,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -898,19 +966,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.GetInstanceRequest, *, @@ -974,7 +1056,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1027,19 +1117,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.ListInstancesRequest, *, @@ -1105,7 +1209,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1158,20 +1270,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: cloud_redis.UpdateInstanceRequest, *, @@ -1238,7 +1364,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1339,35 +1474,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): @@ -1381,19 +1516,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1456,7 +1605,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1494,7 +1651,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): @@ -1508,19 +1665,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1583,7 +1754,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1621,7 +1800,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): @@ -1635,19 +1814,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1707,7 +1900,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1722,7 +1923,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): @@ -1736,19 +1937,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1808,7 +2023,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1823,7 +2046,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): @@ -1837,19 +2060,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -1912,7 +2149,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1950,7 +2195,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): @@ -1964,19 +2209,33 @@ async def _get_response( 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 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), + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2039,7 +2298,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2077,7 +2344,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): @@ -2091,20 +2358,34 @@ async def _get_response( 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 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), - data=body, + body=body, + ) as span: + response = await getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response async def __call__(self, request: operations_pb2.WaitOperationRequest, *, @@ -2167,7 +2448,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2209,3 +2499,9 @@ def kind(self) -> str: async def close(self): await self._session.close() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() 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..ffb1e02dd365 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 @@ -768,6 +768,194 @@ def test_cloud_redis_client_client_options_from_dict(): ) +def test_cloud_redis_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.client._observability", + mock_obs, + ), + mock.patch.object( + transports.CloudRedisGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = CloudRedisClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_cloud_redis_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.client._observability", + mock_obs, + ), + mock.patch.object( + transports.CloudRedisGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = CloudRedisClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_cloud_redis_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -6499,7 +6687,13 @@ def test_unsupported_parameter_rest_asyncio(): credentials=async_anonymous_credentials(), transport="rest_asyncio", client_options=options - ) + ) + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError, match="google.api_core.client_options.ClientOptions.quota_project_id") as exc: # type: ignore + client = CloudRedisClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest_asyncio", + client_options=options + ) def test_transport_grpc_default(): @@ -6557,13 +6751,7 @@ def test_cloud_redis_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_cloud_redis_base_transport_with_credentials_file(): @@ -6593,6 +6781,94 @@ def test_cloud_redis_base_transport_with_adc(): adc.assert_called_once() +def test_cloud_redis_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.redis_v1.services.cloud_redis.transports.CloudRedisTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CloudRedisTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_cloud_redis_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.redis_v1.services.cloud_redis.transports.CloudRedisTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.CloudRedisTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.redis_v1.services.cloud_redis.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_cloud_redis_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: @@ -8290,6 +8566,11 @@ async def test_transport_close_rest_asyncio(): close.assert_not_called() close.assert_called_once() + with mock.patch.object(type(getattr(client.transport, "_session")), "close") as close: + async with client.transport: + close.assert_not_called() + close.assert_called_once() + def test_client_ctx(): transports = [ 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..9abfdbf59c1a 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,16 @@ 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, + record_http_response, +) from google.auth.exceptions import MutualTLSChannelError from google.api_core.universe import EmptyUniverseError @@ -423,3 +432,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..8d42d973c845 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 @@ -15,6 +15,7 @@ # """A compatibility module for older versions of google-api-core.""" +import contextlib import os import json import uuid @@ -32,6 +33,29 @@ from google.protobuf import json_format from urllib.parse import urlparse, urlunparse +# The _observability module was introduced 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] + +if _observability is not None and hasattr(_observability, "trace_http_request"): + trace_http_request = _observability.trace_http_request +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + @contextlib.contextmanager + def trace_http_request(*args: Any, **kwargs: Any): + yield None + +if _observability is not None and hasattr(_observability, "record_http_response"): + record_http_response = _observability.record_http_response +else: # pragma: NO COVER + # Fallback for older versions of google-api-core without HTTP tracing. + def record_http_response(span: Any, response: Any) -> None: + pass + 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..ce0c92d09c69 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,7 +28,14 @@ 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 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, +) from google.cloud.storagebatchoperations_v1._compat import setup_request_id from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore @@ -506,18 +513,29 @@ 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) + ): + client_options = self._client_options + # 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..4e99b03f2882 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 @@ -14,12 +14,14 @@ # limitations under the License. # import abc +import inspect from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.storagebatchoperations_v1 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 @@ -37,6 +39,16 @@ 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 StorageBatchOperationsTransport(abc.ABC): """Abstract transport class for StorageBatchOperations.""" @@ -57,6 +69,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 +100,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 +140,37 @@ 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(self, wrapper, supports_tracing, func, *args, **kwargs): + if supports_tracing: + kwargs["client_options"] = self._client_options + if self.kind: + kwargs["kind"] = self.kind + return wrapper(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.). + # Excluded from coverage because our CI and testing environments always install + # a modern version of google-api-core that supports tracing. + for k in ["client_options", "method_name", "is_streaming", "kind"]: # pragma: NO COVER + kwargs.pop(k, None) # pragma: NO COVER + return wrapper(func, *args, **kwargs) # pragma: NO COVER + + def _wrap_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method.wrap_method, _WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + + def _wrap_async_method(self, func, *args, **kwargs): + return self._wrap(gapic_v1.method_async.wrap_method, _ASYNC_WRAP_METHOD_SUPPORTS_TRACING, func, *args, **kwargs) + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. 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 +183,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 +198,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 +225,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 +240,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 +255,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", ), } @@ -376,7 +426,7 @@ def list_locations(self, @property def kind(self) -> str: - raise NotImplementedError() + return "" __all__ = ( 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..ea716641c461 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 @@ -17,18 +17,24 @@ import logging as std_logging import pickle import warnings -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +import grpc # type: ignore from google.api_core import grpc_helpers + +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] from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib from google.api_core import gapic_v1 +from google.cloud.storagebatchoperations_v1._compat import _observability import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson import google.protobuf.message -import grpc # type: ignore import proto # type: ignore from google.cloud.location import locations_pb2 # type: ignore @@ -138,6 +144,15 @@ 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 +203,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 +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: @@ -265,6 +287,22 @@ 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) + + 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/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..be7b8db8908a 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,7 +13,6 @@ # 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 @@ -25,6 +24,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries from google.api_core import operations_v1 +from google.api_core import client_options as client_options_lib +from google.cloud.storagebatchoperations_v1._compat import _observability from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -185,6 +186,9 @@ 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[aio.ClientInterceptor]] = None, + client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None, + **kwargs, ) -> None: """Instantiate the transport. @@ -236,6 +240,11 @@ 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[aio.ClientInterceptor]]): + Additional 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 +300,8 @@ def __init__(self, *, client_info=client_info, always_use_jwt_access=always_use_jwt_access, api_audience=api_audience, + client_options=client_options, + **kwargs, ) if not self._grpc_channel: @@ -312,10 +323,47 @@ 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 + ): + otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] + channel_interceptors.extend(otel_list) + + # Fallback for older versions of google-api-core where apply_channel_interceptors is unavailable. + def _fallback_apply_interceptors(channel, interceptors): # pragma: NO COVER + 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) and interceptor not in target_list: + 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) and interceptor not in unary_interceptors: + unary_interceptors.append(interceptor) + return channel + + apply_interceptors = getattr( + grpc_helpers_async, + "apply_channel_interceptors", + _fallback_apply_interceptors, + ) + self._grpc_channel = apply_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) @@ -543,6 +591,7 @@ 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, @@ -557,16 +606,19 @@ 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, 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, 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, @@ -581,6 +633,7 @@ 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, @@ -595,6 +648,7 @@ 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, @@ -609,43 +663,51 @@ 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, default_timeout=None, client_info=client_info, + method_name="google.cloud.location.Locations/GetLocation", ), 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: self._wrap_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, default_timeout=None, client_info=client_info, + method_name="google.longrunning.Operations/DeleteOperation", ), 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: self._wrap_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) + """Overrides the base transport's synchronous _wrap_method to proxy + to _wrap_async_method so that RPC calls and retries are wrapped as + asynchronous callables.""" + return self._wrap_async_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..5c63ac4374fe 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 @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import json # type: ignore @@ -23,7 +24,7 @@ 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, record_http_response import google.protobuf from google.protobuf import json_format @@ -42,6 +43,7 @@ from google.longrunning import operations_pb2 # type: ignore +from google.api_core import client_options as client_options_lib from .rest_base import _BaseStorageBatchOperationsRestTransport from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO @@ -495,6 +497,7 @@ class StorageBatchOperationsRestStub: _session: AuthorizedSession _host: str _interceptor: StorageBatchOperationsRestInterceptor + _client_options: Optional[Union[client_options_lib.ClientOptions, dict]] = None class StorageBatchOperationsRestTransport(_BaseStorageBatchOperationsRestTransport): @@ -526,6 +529,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 +574,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 +588,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 +662,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.CancelJobRequest, *, @@ -728,7 +752,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -740,7 +773,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 +809,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.CreateJobRequest, *, @@ -856,7 +902,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -866,7 +921,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 +957,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.DeleteJobRequest, *, @@ -974,7 +1042,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -993,19 +1069,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.GetBucketOperationRequest, *, @@ -1071,7 +1161,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1083,7 +1181,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 +1217,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.GetJobRequest, *, @@ -1197,7 +1308,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1209,7 +1328,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 +1364,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.ListBucketOperationsRequest, *, @@ -1324,7 +1456,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1336,7 +1476,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 +1512,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: storage_batch_operations.ListJobsRequest, *, @@ -1448,7 +1601,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1460,7 +1621,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 +1651,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 +1659,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 +1667,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 +1675,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 +1683,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 +1691,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 +1699,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 +1717,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.GetLocationRequest, *, @@ -1632,7 +1806,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1666,7 +1848,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 +1862,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: locations_pb2.ListLocationsRequest, *, @@ -1755,7 +1951,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1789,7 +1993,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 +2007,34 @@ def _get_response( 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 = 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), - data=body, + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.CancelOperationRequest, *, @@ -1876,7 +2094,16 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1887,7 +2114,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 +2128,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.DeleteOperationRequest, *, @@ -1973,7 +2214,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -1984,7 +2233,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 +2247,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.GetOperationRequest, *, @@ -2073,7 +2336,15 @@ 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), + ) # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. @@ -2107,7 +2378,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 +2392,33 @@ def _get_response( 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 = 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), + body=body, + ) as span: + response = getattr(session, method)( + url, + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), ) - return response + record_http_response(span, response) + return response def __call__(self, request: operations_pb2.ListOperationsRequest, *, @@ -2196,7 +2481,15 @@ 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), + ) # 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..4ff91efd4c3c 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,16 @@ 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, + record_http_response, +) from google.cloud.storagebatchoperations_v1._compat import setup_request_id from google.auth.exceptions import MutualTLSChannelError @@ -528,3 +537,66 @@ 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 span: + pass + + +def test_record_http_response_compat(): + # record_http_response is exposed from _compat and callable with dummy args + record_http_response(None, 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() + mock_obs.record_http_response = 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 + assert reloaded.record_http_response is mock_obs.record_http_response + 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 span: + assert span is None + reloaded.record_http_response(None, None) + finally: + # Restore _compat to normal environment + monkeypatch.undo() + importlib.reload(_compat) 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..e6e09b93a86f 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 @@ -760,6 +760,194 @@ def test_storage_batch_operations_client_client_options_from_dict(): ) +def test_storage_batch_operations_client_otel_channel_injection_enabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = True + with ( + mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.client._observability", + mock_obs, + ), + mock.patch.object( + transports.StorageBatchOperationsGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = StorageBatchOperationsClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + assert called_kwargs.get("client_options") == client._client_options + + +def test_storage_batch_operations_client_otel_channel_injection_disabled(): + mock_obs = mock.Mock() + mock_obs.is_otel_capabilities_enabled.return_value = False + with ( + mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.client._observability", + mock_obs, + ), + mock.patch.object( + transports.StorageBatchOperationsGrpcTransport, "__init__", return_value=None + ) as patched_transport_init, + ): + client = StorageBatchOperationsClient(transport="grpc") + + mock_obs.is_otel_capabilities_enabled.assert_called_once_with(client._client_options) + called_kwargs = patched_transport_init.call_args.kwargs + 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 + + +def test_storage_batch_operations_grpc_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + + with ( + mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.grpc._observability", + mock_obs, + ), + 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_obs.get_otel_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 + + +def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_interceptor(): + mock_otel_interceptor = mock.Mock() + mock_obs = mock.Mock() + mock_obs.get_otel_async_interceptor.return_value = mock_otel_interceptor + mock_channel = mock.Mock() + mock_channel._unary_unary_interceptors = [] + + with ( + mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.grpc_asyncio._observability", + mock_obs, + ), + 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_obs.get_otel_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), @@ -6622,13 +6810,7 @@ def test_storage_batch_operations_base_transport(): with pytest.raises(NotImplementedError): transport.operations_client - # Catch all for all remaining methods and properties - remainder = [ - 'kind', - ] - for r in remainder: - with pytest.raises(NotImplementedError): - getattr(transport, r)() + assert transport.kind == "" def test_storage_batch_operations_base_transport_with_credentials_file(): @@ -6658,6 +6840,94 @@ def test_storage_batch_operations_base_transport_with_adc(): adc.assert_called_once() +def test_storage_batch_operations_base_transport_wrap_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.StorageBatchOperationsTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.StorageBatchOperationsTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_method(func, client_options=options, kind="grpc") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.base._WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + +def test_storage_batch_operations_base_transport_wrap_async_method(): + mock_wrap = mock.Mock() + with mock.patch("google.api_core.gapic_v1.method_async.wrap_method", mock_wrap): + options = client_options.ClientOptions() + with mock.patch.object(google.auth, 'default', autospec=True) as adc, mock.patch('google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.StorageBatchOperationsTransport._prep_wrapped_messages') as prep: + adc.return_value = (ga_credentials.AnonymousCredentials(), None) + transport = transports.StorageBatchOperationsTransport(client_options=options) + + # Mock the kind property to return a value + with mock.patch.object(type(transport), "kind", new_callable=mock.PropertyMock) as mock_kind: + mock_kind.return_value = "grpc_asyncio" + + # Test modern google-api-core with tracing support + with mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + func = mock.Mock() + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert mock_wrap.call_args.kwargs.get("kind") == "grpc_asyncio" + + # Test older google-api-core without tracing support + with mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + False, + ): + mock_wrap.reset_mock() + transport._wrap_async_method(func, client_options=options, kind="grpc_asyncio") + assert "client_options" not in mock_wrap.call_args.kwargs + assert "kind" not in mock_wrap.call_args.kwargs + + # Test for default/empty kind on base transport + with mock.patch( + "google.cloud.storagebatchoperations_v1.services.storage_batch_operations.transports.base._ASYNC_WRAP_METHOD_SUPPORTS_TRACING", + True, + ): + mock_wrap.reset_mock() + mock_kind.return_value = "" + transport._wrap_async_method(func) + assert mock_wrap.call_args.kwargs.get("client_options") == options + assert "kind" not in mock_wrap.call_args.kwargs + + def test_storage_batch_operations_auth_adc(): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, 'default', autospec=True) as adc: diff --git a/packages/gapic-generator/tests/system/conftest.py b/packages/gapic-generator/tests/system/conftest.py index 73169dd8a79f..9a80f1fa589b 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 diff --git a/packages/gapic-generator/tests/system/span_contract.py b/packages/gapic-generator/tests/system/span_contract.py new file mode 100644 index 000000000000..9aace87599a9 --- /dev/null +++ b/packages/gapic-generator/tests/system/span_contract.py @@ -0,0 +1,302 @@ +# 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 Contract Validator for OpenTelemetry Spans. + +Provides formal contract definitions and assertions for validating OpenTelemetry +spans across Tier 3 (Client API Method Spans) and Tier 4 (Transport Wire Spans). + +Key Architectural Principles: +1. Strict Floor vs. Open/Strict Ceiling: + - Tier 3 (Client API Method Spans): Strict floor AND strict ceiling. + Tier 3 is 100% owned by `google-api-core`. No upstream package injects + uncontrolled attributes into Tier 3. Any unexpected attribute indicates + untracked drift or an unvetted addition ("weirdo" attribute). + - Tier 4 (Transport Wire Spans): Strict floor with open ceiling. + Tier 4 is instrumented by `opentelemetry-instrumentation-grpc` (and future + HTTP instrumentors). Upstream Semantic Conventions evolve across minor + releases (e.g., adding `network.transport`, `server.socket.address`). + Enforcing a strict ceiling on Tier 4 would cause brittle test failures + on upstream upgrades. Enforcing a strict floor guarantees that our required + MVP attributes are always present without breaking on upstream churn. +2. Dynamic Namespaces: + - Attributes with dynamic keys (such as `gcp.errors.metadata.` flattened + from Google Cloud `ErrorInfo.metadata`) are validated via `allowed_prefixes`. +3. Actionable Diagnostic Errors: + - Failures explicitly indicate whether required attributes were missing, + forbidden attributes were leaked, unexpected "weirdo" attributes appeared, + or specific values failed equality or custom validator checks. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Callable, Mapping, Set, Tuple + + +@dataclass(frozen=True) +class SpanContract: + """Semantic contract specification for OpenTelemetry spans. + + Attributes: + required: Set of attribute keys that MUST be present on the span. + optional: Set of known attribute keys that MAY be present on the span + (e.g., non-default `server.port`, `gcp.grpc.resend_count`). + allowed_prefixes: Tuple of prefix strings for dynamic attribute keys + (e.g., `("gcp.errors.metadata.",)`). + forbidden: Set of attribute keys that MUST NOT be present on the span + (e.g., legacy `rpc.system` duplicate, or error attributes on success spans). + strict_ceiling: If True, any attribute present on the span that is not in + `required`, `optional`, or matched by `allowed_prefixes` will trigger + a contract violation error. + """ + + required: Set[str] = field(default_factory=set) + optional: Set[str] = field(default_factory=set) + allowed_prefixes: Tuple[str, ...] = field(default_factory=tuple) + forbidden: Set[str] = field(default_factory=set) + strict_ceiling: bool = False + + +# --------------------------------------------------------------------------- +# Pre-defined Contracts +# --------------------------------------------------------------------------- + +# Tier 3 (Client API Method Span) - Success Contract +# Strict floor and strict ceiling. Owned entirely by google-api-core. +T3_SUCCESS_CONTRACT = SpanContract( + required={ + "rpc.system.name", + "rpc.method", + "rpc.response.status_code", + }, + forbidden={ + "gcp.errors.domain", + "error.type", + "status.message", + "rpc.system", + }, + strict_ceiling=True, +) + +# Tier 3 (Client API Method Span) - Error Contract +# Strict floor and strict ceiling with dynamic metadata prefix support. +T3_ERROR_CONTRACT = SpanContract( + required={ + "rpc.system.name", + "rpc.method", + "rpc.response.status_code", + "error.type", + "status.message", + }, + optional={ + "gcp.errors.domain", + }, + allowed_prefixes=("gcp.errors.metadata.",), + forbidden={ + "rpc.system", + }, + strict_ceiling=True, +) + +# Tier 4 (gRPC Transport Wire Span) - Success Contract +# Strict floor, open ceiling (absorbs upstream OTel gRPC semconv additions). +T4_GRPC_SUCCESS_CONTRACT = SpanContract( + required={ + "rpc.system.name", + "rpc.method", + "rpc.response.status_code", + "url.domain", + }, + optional={ + "server.address", + "server.port", + "gcp.grpc.resend_count", + "rpc.service", + "rpc.system", # Emitted natively by upstream opentelemetry-instrumentation-grpc + "rpc.grpc.status_code", + "net.peer.name", + "net.peer.port", + }, + forbidden=set(), + strict_ceiling=False, +) + +# Tier 4 (gRPC Transport Wire Span) - Error Contract +# Strict floor, open ceiling. +T4_GRPC_ERROR_CONTRACT = SpanContract( + required={ + "rpc.system.name", + "rpc.method", + "url.domain", + }, + optional={ + "server.address", + "server.port", + "gcp.grpc.resend_count", + "rpc.service", + "rpc.system", # Emitted natively by upstream opentelemetry-instrumentation-grpc + "rpc.grpc.status_code", + "rpc.response.status_code", + "net.peer.name", + "net.peer.port", + }, + forbidden=set(), + strict_ceiling=False, +) + +# Tier 4 (HTTP Transport Wire Span) - Success Contract +# Strict floor, open ceiling. +T4_HTTP_SUCCESS_CONTRACT = SpanContract( + required={ + "http.request.method", + "http.response.status_code", + "url.domain", + }, + optional={ + "server.address", + "server.port", + "url.template", + "url.full", + "http.request.body.size", + "http.response.body.size", + }, + forbidden={ + "error.type", + "rpc.system.name", + }, + strict_ceiling=False, +) + +# Tier 4 (HTTP Transport Wire Span) - Error Contract +# Strict floor, open ceiling. +T4_HTTP_ERROR_CONTRACT = SpanContract( + required={ + "http.request.method", + "url.domain", + "error.type", + }, + optional={ + "http.response.status_code", + "server.address", + "server.port", + "url.template", + "url.full", + "http.request.body.size", + "http.response.body.size", + "status.message", + }, + forbidden={ + "rpc.system.name", + }, + strict_ceiling=False, +) + + +def assert_span_contract( + span_or_attrs: Any, + contract: SpanContract, + *, + exact_values: Mapping[str, Any] | None = None, + custom_validators: Mapping[str, Callable[[Any], bool]] | None = None, + label: str | None = None, +) -> None: + """Validates an OpenTelemetry span against a semantic contract specification. + + Args: + span_or_attrs: A `ReadableSpan` instance or a dictionary of attribute key-value pairs. + contract: The `SpanContract` defining required, optional, forbidden, and prefix rules. + exact_values: Optional dictionary of attributes that must match exact expected values. + custom_validators: Optional dictionary of attribute keys mapped to predicate callables. + label: Optional human-readable description for debugging (e.g. "T3 Method Span"). + + Raises: + AssertionError: If any contract rule (required, forbidden, ceiling, or value) is violated. + """ + if hasattr(span_or_attrs, "attributes"): + actual_attrs: Mapping[str, Any] = span_or_attrs.attributes or {} + span_name = getattr(span_or_attrs, "name", "unknown") + elif isinstance(span_or_attrs, Mapping): + actual_attrs = span_or_attrs + span_name = "attribute_dict" + else: + raise TypeError( + f"Expected ReadableSpan or Mapping, got {type(span_or_attrs).__name__}" + ) + + context_str = ( + f"[{label}] (span: '{span_name}')" if label else f"(span: '{span_name}')" + ) + actual_keys = set(actual_attrs.keys()) + + # 1. Floor Validation: All required attributes must be present + missing_required = contract.required - actual_keys + if missing_required: + raise AssertionError( + f"{context_str} Span contract floor violation: missing required attributes: " + f"{sorted(missing_required)}. Present attributes: {sorted(actual_keys)}" + ) + + # 2. Forbidden Validation: No forbidden attributes must be present + forbidden_found = contract.forbidden & actual_keys + if forbidden_found: + raise AssertionError( + f"{context_str} Span contract forbidden violation: found disallowed attributes: " + f"{sorted(forbidden_found)}." + ) + + # 3. Ceiling Validation (Drift / 'Weirdo' Detection): + if contract.strict_ceiling: + unrecognized: set[str] = set() + for key in actual_keys: + if key in contract.required or key in contract.optional: + continue + if any(key.startswith(prefix) for prefix in contract.allowed_prefixes): + continue + unrecognized.add(key) + + if unrecognized: + raise AssertionError( + f"{context_str} Span contract ceiling violation: unrecognized / untracked attributes " + f"detected: {sorted(unrecognized)}. If these are intentional, register them in " + f"`required`, `optional`, or `allowed_prefixes` of the contract." + ) + + # 4. Exact Value Validation + if exact_values: + for key, expected_val in exact_values.items(): + if key not in actual_attrs: + raise AssertionError( + f"{context_str} Expected attribute '{key}' not found on span." + ) + actual_val = actual_attrs[key] + if actual_val != expected_val: + raise AssertionError( + f"{context_str} Attribute value mismatch for '{key}': " + f"expected {expected_val!r}, got {actual_val!r}." + ) + + # 5. Custom Validator Predicates + if custom_validators: + for key, validator in custom_validators.items(): + if key not in actual_attrs: + raise AssertionError( + f"{context_str} Expected attribute '{key}' for custom validation not found on span." + ) + actual_val = actual_attrs[key] + if not validator(actual_val): + raise AssertionError( + f"{context_str} Attribute '{key}' with value {actual_val!r} " + f"failed custom validation predicate." + ) 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..412888f0f859 --- /dev/null +++ b/packages/gapic-generator/tests/system/test_tracing.py @@ -0,0 +1,692 @@ +# 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) + +from google import showcase +from google.api_core import exceptions +from google.api_core import retry as retries +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.protobuf import any_pb2 +from google.rpc import code_pb2, error_details_pb2 +from google.showcase import EchoClient + +try: + from google.showcase import EchoAsyncClient + + HAS_ASYNC_CLIENT = True +except ImportError: + HAS_ASYNC_CLIENT = False + +try: + from .conftest import ( + HAS_ASYNC_REST_ECHO_TRANSPORT, + async_anonymous_credentials, + construct_client, + ) + from .span_contract import ( + T3_ERROR_CONTRACT, + T3_SUCCESS_CONTRACT, + T4_GRPC_ERROR_CONTRACT, + T4_GRPC_SUCCESS_CONTRACT, + T4_HTTP_SUCCESS_CONTRACT, + SpanContract, + assert_span_contract, + ) +except (ImportError, ValueError): + from conftest import ( + HAS_ASYNC_REST_ECHO_TRANSPORT, + async_anonymous_credentials, + construct_client, + ) + from span_contract import ( + T3_ERROR_CONTRACT, + T3_SUCCESS_CONTRACT, + T4_GRPC_ERROR_CONTRACT, + T4_GRPC_SUCCESS_CONTRACT, + T4_HTTP_SUCCESS_CONTRACT, + SpanContract, + assert_span_contract, + ) + + +@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 + + +@pytest.fixture +def otel_echo_async_client(span_exporter, use_mtls): + """Constructs an EchoAsyncClient over gRPC wired with an in-memory TracerProvider.""" + if not HAS_ASYNC_CLIENT: + pytest.skip("EchoAsyncClient is not available") + from grpc.experimental import aio + + 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( + EchoAsyncClient, + use_mtls, + transport_name="grpc_asyncio", + channel_creator=aio.insecure_channel, + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + yield client, exporter + + +@pytest.fixture +def otel_echo_rest_client(span_exporter, use_mtls): + """Constructs an EchoClient over REST 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, + transport_name="rest", + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + yield client, exporter + + +@pytest.fixture +def otel_echo_async_rest_client(span_exporter, use_mtls): + """Constructs an EchoAsyncClient over async REST wired with an in-memory TracerProvider.""" + if not HAS_ASYNC_CLIENT or not HAS_ASYNC_REST_ECHO_TRANSPORT: + pytest.skip("EchoAsyncClient or AsyncEchoRestTransport is not available") + 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( + EchoAsyncClient, + use_mtls, + transport_name="rest_asyncio", + client_options=options, + credentials=async_anonymous_credentials(), + ) + yield client, exporter + + +def test_sync_unary_tracing(otel_echo_client): + """Verifies that a synchronous unary RPC generates trace spans conforming to semantic contracts.""" + client, exporter = otel_echo_client + + response = client.echo(showcase.EchoRequest(content="hello world")) + assert response.content == "hello world" + + spans = exporter.get_finished_spans() + # Synchronous unary calls generate both a Tier 3 method span and a Tier 4 wire span + assert len(spans) == 2 + + # Separate Tier 3 method span (root) and Tier 4 wire span (child) + 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] + assert len(t3_spans) == 1 + assert len(t4_spans) == 1 + + method_span = t3_spans[0] + wire_span = t4_spans[0] + + # Validate Tier 3 (Client API Method Span) semantic contract (strict floor & ceiling) + assert_span_contract( + method_span, + T3_SUCCESS_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "OK", + }, + label="T3 Sync Unary Method Span", + ) + assert method_span.name == "google.showcase.v1beta1.Echo/Echo" + assert method_span.kind == trace.SpanKind.CLIENT + + # Validate Tier 4 (Transport Wire Span) semantic contract (strict floor & open ceiling) + assert_span_contract( + wire_span, + T4_GRPC_SUCCESS_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "OK", + "url.domain": "googleapis.com", + }, + label="T4 Sync Unary Wire Span", + ) + assert wire_span.name == "google.showcase.v1beta1.Echo/Echo" + assert wire_span.kind == trace.SpanKind.CLIENT + assert wire_span.parent.span_id == method_span.context.span_id + + +@pytest.mark.asyncio +async def test_async_unary_tracing(otel_echo_async_client): + """Verifies that an async gRPC unary RPC generates trace spans conforming to semantic contracts.""" + client, exporter = otel_echo_async_client + + response = await client.echo(showcase.EchoRequest(content="hello async world")) + assert response.content == "hello async world" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + + 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] + assert len(t3_spans) == 1 + assert len(t4_spans) == 1 + + method_span = t3_spans[0] + wire_span = t4_spans[0] + + assert_span_contract( + method_span, + T3_SUCCESS_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "OK", + }, + label="T3 Async gRPC Method Span", + ) + assert method_span.name == "google.showcase.v1beta1.Echo/Echo" + assert method_span.kind == trace.SpanKind.CLIENT + + assert_span_contract( + wire_span, + T4_GRPC_SUCCESS_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "OK", + "url.domain": "googleapis.com", + }, + label="T4 Async gRPC Wire Span", + ) + assert wire_span.name == "google.showcase.v1beta1.Echo/Echo" + assert wire_span.kind == trace.SpanKind.CLIENT + assert wire_span.parent.span_id == method_span.context.span_id + + +def test_sync_rest_unary_tracing(otel_echo_rest_client): + """Verifies that a synchronous REST RPC generates trace spans conforming to semantic contracts.""" + client, exporter = otel_echo_rest_client + + response = client.echo(showcase.EchoRequest(content="hello sync rest")) + assert response.content == "hello sync rest" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + + 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] + assert len(t3_spans) == 1 + assert len(t4_spans) == 1 + + method_span = t3_spans[0] + wire_span = t4_spans[0] + + assert_span_contract( + method_span, + T3_SUCCESS_CONTRACT, + exact_values={ + "rpc.system.name": "http", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "OK", + }, + label="T3 Sync REST Method Span", + ) + assert method_span.name == "google.showcase.v1beta1.Echo/Echo" + assert method_span.kind == trace.SpanKind.CLIENT + + assert_span_contract( + wire_span, + T4_HTTP_SUCCESS_CONTRACT, + exact_values={ + "http.request.method": "POST", + "http.response.status_code": 200, + "url.domain": "googleapis.com", + }, + label="T4 Sync REST Wire Span", + ) + assert wire_span.name == "POST" + assert wire_span.kind == trace.SpanKind.CLIENT + assert wire_span.parent.span_id == method_span.context.span_id + + +@pytest.mark.asyncio +async def test_async_rest_unary_tracing(otel_echo_async_rest_client): + """Verifies that an async REST RPC generates trace spans conforming to semantic contracts.""" + client, exporter = otel_echo_async_rest_client + + response = await client.echo(showcase.EchoRequest(content="hello async rest")) + assert response.content == "hello async rest" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + + 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] + assert len(t3_spans) == 1 + assert len(t4_spans) == 1 + + method_span = t3_spans[0] + wire_span = t4_spans[0] + + assert_span_contract( + method_span, + T3_SUCCESS_CONTRACT, + exact_values={ + "rpc.system.name": "http", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "OK", + }, + label="T3 Async REST Method Span", + ) + assert method_span.name == "google.showcase.v1beta1.Echo/Echo" + assert method_span.kind == trace.SpanKind.CLIENT + + assert_span_contract( + wire_span, + T4_HTTP_SUCCESS_CONTRACT, + exact_values={ + "http.request.method": "POST", + "http.response.status_code": 200, + "url.domain": "googleapis.com", + }, + label="T4 Async REST Wire Span", + ) + assert wire_span.name == "POST" + assert wire_span.kind == trace.SpanKind.CLIENT + assert wire_span.parent.span_id == method_span.context.span_id + + +def test_unary_retries_tracing(otel_echo_client): + """Verifies that each attempt of a retried RPC generates a separate span satisfying contracts.""" + client, exporter = otel_echo_client + + # Configure a custom retry policy with 2 attempts on DeadlineExceeded + custom_retry = retries.Retry( + predicate=retries.if_exception_type(exceptions.DeadlineExceeded), + initial=0.05, + maximum=0.1, + multiplier=1.0, + deadline=0.3, + ) + + with pytest.raises((exceptions.DeadlineExceeded, exceptions.RetryError)): + client.echo( + { + "error": { + "code": code_pb2.Code.Value("DEADLINE_EXCEEDED"), + "message": "Simulated deadline exceeded error for retry testing.", + }, + }, + retry=custom_retry, + ) + + spans = exporter.get_finished_spans() + # At least two attempts should have been made and recorded + assert len(spans) >= 2 + + # Separate Tier 3 method span (root) and Tier 4 attempt wire spans (children) + parent_spans = [s for s in spans if s.parent is None] + child_spans = [s for s in spans if s.parent is not None] + assert len(parent_spans) == 1 + assert len(child_spans) >= 1 + + parent_span = parent_spans[0] + + # Validate Tier 3 Parent Method Span Contract (Error) + assert_span_contract( + parent_span, + T3_ERROR_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "DEADLINE_EXCEEDED", + "error.type": "DEADLINE_EXCEEDED", + }, + custom_validators={ + "status.message": lambda msg: "Simulated deadline exceeded error" in msg, + }, + label="T3 Unary Retries Parent Error Span", + ) + + # Validate each child T4 Wire Span + for idx, child_span in enumerate(child_spans): + assert_span_contract( + child_span, + T4_GRPC_ERROR_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "url.domain": "googleapis.com", + }, + label=f"T4 Unary Retry Wire Attempt {idx + 1}", + ) + assert child_span.parent.span_id == parent_span.context.span_id + + +def test_unretryable_error_tracing_contract(otel_echo_client): + """Verifies that an unretryable error with rich ErrorInfo satisfies T3 and T4 semantic contracts.""" + client, exporter = otel_echo_client + + err_info = error_details_pb2.ErrorInfo( + reason="RESOURCE_PROJECT_INVALID", + domain="googleapis.com", + metadata={"service": "echo.googleapis.com", "quota_limit": "100"}, + ) + detail_any = any_pb2.Any() + detail_any.Pack(err_info) + + with pytest.raises(exceptions.InvalidArgument): + client.echo( + { + "error": { + "code": code_pb2.Code.Value("INVALID_ARGUMENT"), + "message": "Simulated unretryable invalid argument error.", + "details": [detail_any], + }, + }, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + + 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] + assert len(t3_spans) == 1 + assert len(t4_spans) == 1 + + method_span = t3_spans[0] + wire_span = t4_spans[0] + + # Validate Tier 3 method span with complete set of rich ErrorInfo attributes + assert_span_contract( + method_span, + T3_ERROR_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "rpc.response.status_code": "INVALID_ARGUMENT", + "error.type": "RESOURCE_PROJECT_INVALID", + "gcp.errors.domain": "googleapis.com", + "gcp.errors.metadata.service": "echo.googleapis.com", + "gcp.errors.metadata.quota_limit": "100", + }, + custom_validators={ + "status.message": lambda msg: "Simulated unretryable invalid argument error." + in msg, + }, + label="T3 Unretryable Error with ErrorInfo", + ) + + # Validate Tier 4 wire span + assert_span_contract( + wire_span, + T4_GRPC_ERROR_CONTRACT, + exact_values={ + "rpc.system.name": "grpc", + "rpc.method": "google.showcase.v1beta1.Echo/Echo", + "url.domain": "googleapis.com", + }, + label="T4 Unretryable Error Wire Span", + ) + assert wire_span.parent.span_id == method_span.context.span_id + + +def test_span_contract_validator_diagnostics(): + """Verifies that assert_span_contract detects missing, forbidden, and weirdo attributes.""" + sample_contract = SpanContract( + required={"rpc.system.name", "rpc.method"}, + optional={"optional.tag"}, + allowed_prefixes=("gcp.errors.metadata.",), + forbidden={"rpc.system"}, + strict_ceiling=True, + ) + + # Valid span attributes + valid_attrs = { + "rpc.system.name": "grpc", + "rpc.method": "Showcase/Echo", + "optional.tag": "val", + "gcp.errors.metadata.key": "123", + } + assert_span_contract(valid_attrs, sample_contract) + + # Missing required attribute triggers floor violation + missing_attrs = {"rpc.method": "Showcase/Echo"} + with pytest.raises(AssertionError, match="missing required attributes"): + assert_span_contract(missing_attrs, sample_contract) + + # Forbidden attribute triggers forbidden violation + forbidden_attrs = dict(valid_attrs, **{"rpc.system": "grpc"}) + with pytest.raises(AssertionError, match="found disallowed attributes"): + assert_span_contract(forbidden_attrs, sample_contract) + + # Unexpected 'weirdo' attribute triggers ceiling violation + weirdo_attrs = dict(valid_attrs, **{"untracked.weirdo": "oops"}) + with pytest.raises( + AssertionError, match="unrecognized / untracked attributes detected" + ): + assert_span_contract(weirdo_attrs, sample_contract) + + +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/gapic-generator/tests/unit/schema/test_api.py b/packages/gapic-generator/tests/unit/schema/test_api.py index 13ca7a009c86..0bef9ad02560 100644 --- a/packages/gapic-generator/tests/unit/schema/test_api.py +++ b/packages/gapic-generator/tests/unit/schema/test_api.py @@ -2836,6 +2836,9 @@ def test_mixin_api_signatures(): api_schema = api.API.build(fd, "google.example.v1", opts=opts) res = api_schema.mixin_api_signatures assert res == mixins.MIXINS_MAP + assert res["GetOperation"].rpc_name == "google.longrunning.Operations/GetOperation" + assert res["GetIamPolicy"].rpc_name == "google.iam.v1.IAMPolicy/GetIamPolicy" + assert res["GetLocation"].rpc_name == "google.cloud.location.Locations/GetLocation" def test_mixin_http_options(): diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 2d8c50acbfa9..e8e7ed43b34f 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -18,8 +18,10 @@ from __future__ import annotations +import asyncio +import contextlib import urllib.parse -from typing import TYPE_CHECKING, Any, Callable, Sequence +from typing import TYPE_CHECKING, Any, Callable, Iterator, Sequence from google.api_core import _feature_gating_helpers from google.api_core.client_options import ClientOptions @@ -42,8 +44,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 +74,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 +124,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 +187,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 +211,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 +231,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 +255,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 +265,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 +286,264 @@ def get_otel_async_interceptor( request_hook=request_hook, response_hook=_grpc_client_response_hook, ) + + +_TRACE_CONTEXT_PROPAGATOR: Any = None + + +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 `start_http_span` context manager deliberately supports two distinct invocation styles: +# 1. Bundled Request Object: `start_http_span(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: `start_http_span(method=..., url=..., headers=..., body=...)` +# Used by `trace_http_request` and 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. +@contextlib.contextmanager +def start_http_span( + 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, +): + """Context manager for tracing an HTTP wire request with OpenTelemetry. + + Supports two calling conventions: + - Pass a single `request` object (such as `requests.PreparedRequest`). + - Pass explicit keyword arguments (`method`, `url`, `headers`, `body`, `client_options`). + + Injects W3C traceparent headers into request headers and attaches standard + semantic attributes. If tracing is disabled or OpenTelemetry is not installed, + yields None. + + Args: + request (Optional[Any]): Optional HTTP request object with .method, .url, .headers, and .body. + method (Optional[str]): HTTP request method (e.g. 'GET', 'POST'). + url (Optional[str]): Full request URL. + url_template (Optional[str]): Low-cardinality URL path template (e.g. '/v1/{name}:echo'). + headers (Optional[dict[str, Any]]): Outgoing HTTP headers dictionary for traceparent injection. + body (Optional[Any]): HTTP request body payload. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): Client options used for + feature gating and tracer extraction. + + Yields: + Optional[Span]: The active OpenTelemetry span or None. + """ + # Defensively handle case where client_options was passed as the first positional argument + if ( + isinstance(request, (ClientOptions, dict)) + and client_options is None + and method is not None + ): + client_options = request + request = None + + if not is_otel_capabilities_enabled(client_options): + yield None + return + + try: + from opentelemetry import trace + + tracer_provider = _get_tracer_provider(client_options) + if tracer_provider is not None: + tracer = tracer_provider.get_tracer("google.api_core") + else: + tracer = trace.get_tracer("google.api_core") + + # Resolve request attributes from either bundled object or explicit keyword arguments + 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) + + with tracer.start_as_current_span( + span_name, + kind=trace.SpanKind.CLIENT, + attributes=span_attributes, + ) as span: + if resolved_headers is not None and hasattr( + resolved_headers, "__setitem__" + ): + try: + _get_trace_context_propagator().inject(resolved_headers) + except Exception: # Fail-open on header injection failure + pass + + yield span + except ( + Exception + ): # Fail-open: telemetry failures must never disrupt core RPC execution + yield None + + +def record_http_response(span: Any, response: Any) -> None: + """Record HTTP response attributes on the wire span. + + Args: + span (Optional[Any]): The active OpenTelemetry span. + response (Any): The HTTP response object (e.g. requests.Response). + """ + 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 + + +def record_http_error(span: Any, exc: BaseException) -> None: + """Record an HTTP error/exception on the wire span. + + Args: + span (Optional[Any]): The active OpenTelemetry span. + exc (BaseException): The exception raised during dispatch. + """ + if span 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 + + +@contextlib.contextmanager +def trace_http_request( + *, + 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, +) -> Iterator[Any]: + """Context manager for tracing HTTP wire attempts with automatic error capture. + + Starts an OpenTelemetry span via `start_http_span`, yields the span, and + automatically records any exception raised during the attempt using + `record_http_error` before re-raising. + + Args: + method (Optional[str]): HTTP request method (e.g. 'GET', 'POST'). + url (Optional[str]): Full request URL. + url_template (Optional[str]): Low-cardinality URL path template (e.g. '/v1/{name}:echo'). + headers (Optional[dict[str, Any]]): Outgoing HTTP headers dictionary for traceparent injection. + body (Optional[Any]): HTTP request body payload. + client_options (Optional[Union[ClientOptions, dict[str, Any]]]): Client options used for + feature gating and tracer extraction. + + Yields: + Optional[Span]: The active OpenTelemetry span or None. + """ + with start_http_span( + method=method, + url=url, + url_template=url_template, + headers=headers, + body=body, + client_options=client_options, + ) as span: + try: + yield span + except (Exception, asyncio.CancelledError) as exc: + record_http_error(span, exc) + raise 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 dcb09f18fea2..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 @@ -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..9ed2fd61a177 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import contextlib import sys from unittest import mock @@ -542,3 +543,609 @@ 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_start_http_span_disabled(): + """Proves that start_http_span yields None when tracing is disabled.""" + request = mock.Mock(method="GET", url="https://example.com/api", headers={}) + with _observability.start_http_span( + request, client_options=ClientOptions() + ) as span: + assert span is None + + +def test_start_http_span_active(monkeypatch): + """Proves that start_http_span 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.start_http_span( + request, url_template="/v1/test", client_options=options + ) as span: + assert 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_record_http_response_success(monkeypatch): + """Proves that record_http_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) + + _observability.record_http_response(mock_span, 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_record_http_response_error_status(monkeypatch): + """Proves that record_http_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) + + _observability.record_http_response(mock_span, response) + mock_span.set_attribute.assert_any_call("http.response.status_code", 503) + mock_span.set_status.assert_called_once() + + +def test_record_http_error(monkeypatch): + """Proves that record_http_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) + + _observability.record_http_error(mock_span, 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") + + +def test_start_http_span_with_kwargs(monkeypatch): + """Proves that start_http_span 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.start_http_span( + method="post", + url="https://custom.googleapis.com:8443/v1/test", + headers=headers, + body="string-payload", + url_template="/v1/test", + client_options=options, + ) as span: + assert 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_start_http_span_first_arg_client_options(monkeypatch): + """Proves that start_http_span shifts client_options when passed as first positional arg.""" + 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.instrumentation.grpc", + mock.Mock(), + ) + + options = ClientOptions( + api_endpoint="custom.googleapis.com:8443", + tracer_provider=mock_provider, + ) + + with _observability.start_http_span( + options, + method="GET", + url="https://custom.googleapis.com:8443/v1/test", + ) as span: + assert span is mock_span + + +def test_start_http_span_default_tracer_and_url_parse(monkeypatch): + """Proves that start_http_span 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.start_http_span( + client_options=options, + method="GET", + url="https://parsed-host.org:9443/v1/items", + ) as span: + assert 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_start_http_span_propagator_error(monkeypatch): + """Proves that start_http_span 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.start_http_span( + client_options=options, + method="GET", + url="https://example.com", + headers=headers, + ) as span: + assert span is mock_span + + +def test_start_http_span_unexpected_error(monkeypatch): + """Proves that start_http_span 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.start_http_span( + client_options=options, + method="GET", + url="https://example.com", + ) as span: + assert span is None + + +def test_record_http_response_none_or_missing_attribute(): + """Proves that record_http_response handles None or non-span gracefully.""" + _observability.record_http_response(None, mock.Mock()) + _observability.record_http_response(object(), mock.Mock()) + + +def test_record_http_response_content_fallback_and_invalid_content_length(monkeypatch): + """Proves that record_http_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) + + _observability.record_http_response(mock_span, 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" + ) + _observability.record_http_response(mock_span, response_with_content) + mock_span.set_attribute.assert_called_once_with( + "http.response.body.size", len(b"hello-content") + ) + + +def test_record_http_response_exception_handled(monkeypatch): + """Proves that record_http_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) + + # Should not raise + _observability.record_http_response( + mock_span, mock.Mock(status_code=200, headers={}) + ) + + +def test_record_http_error_none_span(): + """Proves that record_http_error handles span=None gracefully.""" + _observability.record_http_error(None, ValueError("test")) + + +def test_record_http_error_with_status_code_and_empty_msg(monkeypatch): + """Proves that record_http_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) + + _observability.record_http_error(mock_span, 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_record_http_error_exception_handled(monkeypatch): + """Proves that record_http_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 + _observability.record_http_error(mock_span, ValueError("test")) + + +def test_start_http_span_url_parse_exception(monkeypatch): + """Proves that start_http_span 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("urllib.parse.urlsplit", side_effect=ValueError("Invalid URL")): + options = ClientOptions() + with _observability.start_http_span( + client_options=options, + method="GET", + url="http://[invalid-url", + ) as span: + assert span is mock_span + + +def test_start_http_span_empty_url(monkeypatch): + """Proves that start_http_span 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.start_http_span( + client_options=options, + method="GET", + url="", + ) as span: + assert span is mock_span + + +def test_record_http_response_content_len_error(monkeypatch): + """Proves record_http_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() + + _observability.record_http_response(mock_span, response) + + +def test_record_http_error_partial_span(monkeypatch): + """Proves that record_http_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() + _observability.record_http_error(span, 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() + _observability.record_http_error(span2, ValueError("no attr span")) + assert span2.recorded is True + + +def test_record_http_response_no_content_length_and_no_content(monkeypatch): + """Proves that record_http_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={}) + _observability.record_http_response(mock_span, response) + mock_span.set_attribute.assert_called_once_with("http.response.status_code", 200) + + +def test_trace_http_request_disabled(): + """Proves that trace_http_request yields None when tracing is disabled.""" + headers = {} + with _observability.trace_http_request( + method="GET", + url="https://example.com/api", + headers=headers, + client_options=ClientOptions(), + ) as span: + assert span is None + + +def test_trace_http_request_success(monkeypatch): + """Proves that trace_http_request yields active span on success.""" + 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(), + ) + + headers = {} + with _observability.trace_http_request( + method="POST", + url="https://example.com/api", + headers=headers, + body=b"payload", + url_template="/api", + ) as span: + assert span is mock_span + + +def test_trace_http_request_records_error_and_reraises(monkeypatch): + """Proves that trace_http_request records error on active span when exception occurs.""" + mock_span = mock.MagicMock() + + @contextlib.contextmanager + def mock_start_http_span(**kwargs): + yield mock_span + + monkeypatch.setattr(_observability, "start_http_span", mock_start_http_span) + record_error_called = [] + + def mock_record_http_error(span, exc): + record_error_called.append((span, exc)) + + monkeypatch.setattr(_observability, "record_http_error", mock_record_http_error) + + 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 + + assert len(record_error_called) == 1 + assert record_error_called[0] == (mock_span, err)