-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(gapic): add OpenTelemetry channel tracing to generator templates #18342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bfb40b0
87ca9b8
451e17b
29de72e
7f6519f
b0de0a4
ac095c5
ef7d77d
aa2bead
894b380
1c1b9af
81b686c
324b866
abeaf04
99a4d3d
4b82c9c
0fb354a
acce308
d337933
13f1218
11ccd0d
a7dad4f
858ff52
b1c66e4
b409ba6
2059f32
66a6f0e
7ebebfa
a7956a5
cd5299a
8b540c8
880f5b9
9bb3305
af22028
d073f2b
715f58b
c13f97a
b1b7ce4
ce65620
d53607b
f7f3feb
2f522bd
ee5f778
89675ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ from collections import OrderedDict | |
| import functools | ||
| {% endif %} | ||
| from http import HTTPStatus | ||
| import inspect | ||
| import json | ||
| import logging as std_logging | ||
| import os | ||
|
|
@@ -53,6 +54,13 @@ try: | |
| except ImportError: # pragma: NO COVER | ||
| CLIENT_LOGGING_SUPPORTED = False | ||
|
|
||
| # Optional: OpenTelemetry tracing capabilities for grpc channel injection | ||
| # Note: _observability was added in google-api-core 2.36.0+; guard for older versions | ||
| try: | ||
| from google.api_core import _observability # type: ignore[attr-defined] | ||
| except ImportError: # pragma: NO COVER | ||
| _observability = None # type: ignore[assignment] | ||
|
|
||
| _LOGGER = std_logging.getLogger(__name__) | ||
|
|
||
| {% filter sort_lines %} | ||
|
|
@@ -314,17 +322,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 +363,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 +405,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 +423,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 +481,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, | ||
|
|
@@ -531,19 +539,34 @@ 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) | ||
| and ( | ||
| not isinstance(transport_init, type) | ||
| or issubclass(transport_init, {{ service.grpc_transport_name }}) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have to be so selective about which transports we pass client_options to? I was thinking by moving the interceptor to the transport, we could pass the same init args to them all (I wonder if grpc_transport_name here could cause problems specifically, if there are any http-only clients without this set) |
||
| ) | ||
| ): | ||
| 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 +850,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() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,15 @@ | |
| {% block content %} | ||
|
|
||
| import abc | ||
| import inspect | ||
| from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union | ||
|
|
||
| {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} | ||
| from {{package_path}} import gapic_version as package_version | ||
|
|
||
| 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,13 @@ 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_method supports | ||
| # 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 | ||
| ) | ||
|
|
||
|
|
||
| class {{ service.name }}Transport(abc.ABC): | ||
| """Abstract transport class for {{ service.name }}.""" | ||
|
|
@@ -75,6 +84,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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be added to http too, to be consistent? We should at least add a **kwarg, so new arguments can be passed through? |
||
| **kwargs, | ||
| ) -> None: | ||
| """Instantiate the transport. | ||
|
|
@@ -105,6 +115,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 +158,39 @@ class {{ service.name }}Transport(abc.ABC): | |
| host += ':443' | ||
| self._host = host | ||
|
|
||
| self._client_options = client_options | ||
| self._wrap_with_tracing = _WRAP_METHOD_SUPPORTS_TRACING | ||
|
|
||
| self._wrapped_methods: Dict[Callable, Callable] = {} | ||
|
|
||
| @property | ||
| def host(self): | ||
| return self._host | ||
|
|
||
| def _wrap_method(self, func, *args, **kwargs): | ||
| if self._wrap_with_tracing: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can't this just be |
||
| kwargs["client_options"] = self._client_options | ||
| try: | ||
| kwargs["kind"] = self.kind | ||
| # The abstract BaseTransport class raises NotImplementedError for the kind property. | ||
| # Concrete transport subclasses (gRPC, REST) override kind, so this exception handler | ||
| # is unreachable during normal execution. Excluded from coverage check. | ||
| except NotImplementedError: # pragma: NO COVER | ||
| pass | ||
| return gapic_v1.method.wrap_method(func, *args, **kwargs) | ||
| # The fallback below strips tracing-specific arguments when an older version | ||
| # of google-api-core is installed (which does not accept client_options, etc.). | ||
| # 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 gapic_v1.method.wrap_method(func, *args, **kwargs) # pragma: NO COVER | ||
|
|
||
| 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 +213,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() #} | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to be unused