Repository navigation
fix(gapic-generator): default client_options.api_endpoint to transport host for OpenTelemetry spans - #18869
fix(gapic-generator): default client_options.api_endpoint to transport host for OpenTelemetry spans#18869chalmerlowe wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the base transport templates and generated golden files to automatically populate the api_endpoint in client_options with the transport's host if it is not already set, supporting both ClientOptions instances and dictionaries. It also updates the corresponding unit tests and telemetry requirements matrix. A review comment correctly points out that mutating client_options in-place can cause unexpected side effects if the caller reuses the options object, and suggests copying the options before modification.
| if isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: | ||
| client_options.api_endpoint = self._host | ||
| elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): | ||
| client_options["api_endpoint"] = self._host |
There was a problem hiding this comment.
Mutating the client_options argument in-place (whether it is a ClientOptions instance or a dict) can lead to unexpected side effects if the caller reuses the same options object across multiple client or transport instances. To prevent this, we should create a copy of the options before modifying them.
if isinstance(client_options, client_options_lib.ClientOptions):
if not client_options.api_endpoint:
import copy
client_options = copy.copy(client_options)
client_options.api_endpoint = self._host
elif isinstance(client_options, dict):
if not client_options.get("api_endpoint"):
client_options = dict(client_options)
client_options["api_endpoint"] = self._host
There was a problem hiding this comment.
This seems like a fair point, we should probably copy the options (if we're not already)
There was a problem hiding this comment.
Fixed in 81e79e2a791 and 8ac1e81836f. We now shallow-copy client_options (copy.copy(client_options)) for both ClientOptions instances and dict inputs before defaulting api_endpoint to self._host, and we added unit test assertions verifying that the caller's original ClientOptions or dict is not mutated in-place. We also added handling for client_options=None when OpenTelemetry tracing is enabled so direct transport instantiation populates api_endpoint as well.
There was a problem hiding this comment.
UPDATED/RESOLVED
With the switch to passing default_host=self._default_host instead of reading/modifying client_options.api_endpoint in base.py.j2, base.py.j2 no longer needs any conditional inspection of client_options (it's back to a single self._client_options = client_options assignment), and _observability._extract_endpoint_attributes handles dict coercion via client_options_lib.from_dict if a dict is passed directly to a transport.
See this comment for additional info/clarification: we moved away from changing api_endpoint.
#18869 (comment)
5591f09 to
3c63f33
Compare
f636b0e to
a8fbedc
Compare
3c63f33 to
63c5e32
Compare
a8fbedc to
fd9c5ba
Compare
63c5e32 to
aded2a1
Compare
6855304 to
64d36b7
Compare
7568958 to
781a900
Compare
64d36b7 to
3cac2f0
Compare
781a900 to
aeacfb7
Compare
3cac2f0 to
7ecc47d
Compare
aeacfb7 to
813aa30
Compare
7ecc47d to
e5c1686
Compare
813aa30 to
91cde6b
Compare
e5c1686 to
a0113ae
Compare
91cde6b to
d6f0e97
Compare
a0113ae to
7edc460
Compare
d6f0e97 to
9bca1d0
Compare
7edc460 to
dde7880
Compare
9bca1d0 to
07e6fa4
Compare
…t host for OpenTelemetry spans
07e6fa4 to
81e79e2
Compare
…el interceptor tests
| # shallow-copying before populating to avoid mutating caller-owned state. | ||
| elif isinstance(client_options, client_options_lib.ClientOptions) and not client_options.api_endpoint: | ||
| client_options = copy.copy(client_options) | ||
| client_options.api_endpoint = self._host |
There was a problem hiding this comment.
There's a an edge case here: the host argument is only used to build new channels. But grpc transports can be passed in custom channels, which may point at a different host than the one here. So we may be reporting different data than the channel actually sends.
Is it possible to add some logic to the grpc transport, to disable this override when a channel is passed in directly? (_ignore_credentials seems to already be doing something similar)
There was a problem hiding this comment.
But depending on how this data is consumed later, maybe this isn't a big problem
There was a problem hiding this comment.
RESOLVED
I appreciate you pointing out this edge case. Because grpc and grpc_asyncio transports set self._grpc_channel = channel prior to calling super().__init__(...) whenever a pre-constructed grpc.Channel / grpc.aio.Channel instance is passed in, we now check getattr(self, "_grpc_channel", None) is not None in base.py.j2:
self._default_host = (
None if getattr(self, "_grpc_channel", None) is not None else self._host
)When a custom channel is injected directly, self._default_host is None, so we don't report a potentially mismatched default server.address / server.port on the spans (while still honoring client_options.api_endpoint if the user explicitly set it). We also added unit tests for sync, async, and base transports to verify this behavior.
There was a problem hiding this comment.
Checking for _grpc_channel doesn't seem like a great approach to me. We expect it to be set by all grpc transports, whether passed in or not, it's just an implementation detail that it happens to be unset at this point in the execution. But that could easily change in the future, without warning
I think we should set an explicit user_provided_channel flag (like self._ignore_credentials), and use that for this conditional
There was a problem hiding this comment.
Other than that, this approach LGTM
…tating client_options.api_endpoint
…align Showcase test helpers
Summary
Populates
client_options.api_endpointwith the transport's resolvedself._hostinTransport.__init__(base.py.j2) whenclient_optionsis provided without an explicitapi_endpointoverride.Why
_extract_endpoint_attributesingoogle-api-coreonly populatedserver.addressand non-defaultserver.porton Tier 3 (GAPIC client) and gRPC Tier 4 (wire attempt) spans when the caller explicitly passedClientOptions(api_endpoint=...).DEFAULT_HOST(or ahostpassed directly to the transport constructor) without settingclient_options.api_endpoint,server.addressandserver.portwere omitted from Tier 3 and gRPC Tier 4 spans.client_options.api_endpointtoself._hostin the base transport constructor ensures Tier 3 and gRPC Tier 4 spans consistently recordserver.addressand non-defaultserver.port.Files Modified
packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2packages/gapic-generator/tests/system/telemetry_requirements_matrix.csvpackages/gapic-generator/tests/integration/goldens/**