Skip to content

fix(gapic-generator): default client_options.api_endpoint to transport host for OpenTelemetry spans - #18869

Open
chalmerlowe wants to merge 6 commits into
mainfrom
fix/otel-default-host-endpoint-attributes
Open

chalmerlowe wants to merge 6 commits into
mainfrom
fix/otel-default-host-endpoint-attributes

Conversation

@chalmerlowe

@chalmerlowe chalmerlowe commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Populates client_options.api_endpoint with the transport's resolved self._host in Transport.__init__ (base.py.j2) when client_options is provided without an explicit api_endpoint override.

Why

  • Previously, _extract_endpoint_attributes in google-api-core only populated server.address and non-default server.port on Tier 3 (GAPIC client) and gRPC Tier 4 (wire attempt) spans when the caller explicitly passed ClientOptions(api_endpoint=...).
  • When a client or transport connected using the service's DEFAULT_HOST (or a host passed directly to the transport constructor) without setting client_options.api_endpoint, server.address and server.port were omitted from Tier 3 and gRPC Tier 4 spans.
  • Defaulting client_options.api_endpoint to self._host in the base transport constructor ensures Tier 3 and gRPC Tier 4 spans consistently record server.address and non-default server.port.

Files Modified

  • packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2
  • packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2
  • packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv
  • packages/gapic-generator/tests/integration/goldens/**

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +165 to +168
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a fair point, we should probably copy the options (if we're not already)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chalmerlowe chalmerlowe Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 5591f09 to 3c63f33 Compare October 27, 2026 20:37
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from f636b0e to a8fbedc Compare October 27, 2026 20:37
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 3c63f33 to 63c5e32 Compare October 27, 2026 23:03
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from a8fbedc to fd9c5ba Compare October 27, 2026 23:03
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 63c5e32 to aded2a1 Compare October 28, 2026 09:27
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch 2 times, most recently from 6855304 to 64d36b7 Compare October 28, 2026 09:39
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch 2 times, most recently from 7568958 to 781a900 Compare October 28, 2026 21:03
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from 64d36b7 to 3cac2f0 Compare October 28, 2026 21:03
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 781a900 to aeacfb7 Compare October 29, 2026 10:13
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from 3cac2f0 to 7ecc47d Compare October 29, 2026 10:13
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from aeacfb7 to 813aa30 Compare October 29, 2026 11:29
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from 7ecc47d to e5c1686 Compare October 29, 2026 11:29
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 813aa30 to 91cde6b Compare October 29, 2026 13:37
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from e5c1686 to a0113ae Compare October 29, 2026 13:37
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 91cde6b to d6f0e97 Compare October 29, 2026 14:01
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from a0113ae to 7edc460 Compare October 29, 2026 14:01
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from d6f0e97 to 9bca1d0 Compare October 29, 2026 14:19
@chalmerlowe
chalmerlowe force-pushed the fix/otel-http-t3-t4-attributes branch from 7edc460 to dde7880 Compare October 29, 2026 14:19
Base automatically changed from fix/otel-http-t3-t4-attributes to main October 29, 2026 14:49
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 9bca1d0 to 07e6fa4 Compare October 29, 2026 14:59
@chalmerlowe
chalmerlowe force-pushed the fix/otel-default-host-endpoint-attributes branch from 07e6fa4 to 81e79e2 Compare October 30, 2026 14:38
@chalmerlowe
chalmerlowe marked this pull request as ready for review October 30, 2026 16:54
@chalmerlowe
chalmerlowe requested a review from a team as a code owner October 30, 2026 16:54
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But depending on how this data is consumed later, maybe this isn't a big problem

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than that, this approach LGTM

@chalmerlowe chalmerlowe added the automerge Merge the pull request once unit tests and other checks pass. label Nov 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge Merge the pull request once unit tests and other checks pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants