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 9c93aaea9c1d..9546b58ca1fb 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,11 +3,13 @@ {% block content %} import abc +import copy from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}} import gapic_version as package_version from {{package_path}}._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -162,6 +164,24 @@ class {{ service.name }}Transport(abc.ABC): host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 136319c7ced0..dd69c549fbac 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 @@ -996,7 +996,7 @@ def test_{{ service.name|snake_case }}_grpc_transport_otel_channel_interceptor() client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -1070,7 +1070,9 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_inter client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -2498,6 +2500,80 @@ def test_{{ service.name|snake_case }}_base_transport_wrap_method_modern_api_cor assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_{{ service.name|snake_case }}_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.{{ service.name }}Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_{{ service.name|snake_case }}_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.{{ service.name }}Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_{{ service.name|snake_case }}_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.{{ service.name }}Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_{{ service.name|snake_case }}_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 515c173224af..a40ac40c6553 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.asset_v1 import gapic_version as package_version from google.cloud.asset_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -133,6 +135,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 17fc4c8a5c07..a31872a61b88 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 @@ -851,7 +851,7 @@ def test_asset_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -925,7 +925,9 @@ def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -18433,6 +18435,80 @@ def test_asset_service_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_asset_service_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.AssetServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_asset_service_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.AssetServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_asset_service_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.AssetServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_asset_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 9ad2c974a10c..39d1b8bf9763 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.iam.credentials_v1 import gapic_version as package_version from google.iam.credentials_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -130,6 +132,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 f39f23ababfa..d3af824d005c 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 @@ -841,7 +841,7 @@ def test_iam_credentials_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -915,7 +915,9 @@ def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4479,6 +4481,80 @@ def test_iam_credentials_base_transport_wrap_method_modern_api_core(monkeypatch) assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_iam_credentials_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.IAMCredentialsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_iam_credentials_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.IAMCredentialsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_iam_credentials_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.IAMCredentialsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_iam_credentials_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 963fc7ddb134..e4a5b3ecae76 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.eventarc_v1 import gapic_version as package_version from google.cloud.eventarc_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -145,6 +147,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 b5e055c25ec9..556b3265ab3b 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 @@ -872,7 +872,7 @@ def test_eventarc_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -946,7 +946,9 @@ def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -33536,6 +33538,80 @@ def test_eventarc_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_eventarc_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.EventarcTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_eventarc_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.EventarcTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_eventarc_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.EventarcTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_eventarc_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 4a5dbf0b6ca1..cb1732a8c974 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,6 +138,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 4f1882897e01..c0de6b00c70b 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,6 +138,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 98e6b8826af6..630294a64d2e 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,6 +138,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 acea90dd0a47..790030306491 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 @@ -827,7 +827,7 @@ def test_config_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -901,7 +901,9 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -14030,6 +14032,80 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_config_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 433ff946bd8b..a123afd9d2c2 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 @@ -828,7 +828,7 @@ def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -902,7 +902,9 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4470,6 +4472,80 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_logging_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 c3809e05816a..5eee17c5025f 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 @@ -826,7 +826,7 @@ def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -900,7 +900,9 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4270,6 +4272,80 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_metrics_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 4a5dbf0b6ca1..cb1732a8c974 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,6 +138,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 4f1882897e01..c0de6b00c70b 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,6 +138,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 98e6b8826af6..630294a64d2e 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.logging_v2 import gapic_version as package_version from google.cloud.logging_v2._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -136,6 +138,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 5d7d971258ae..1bbf2dd15340 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 @@ -827,7 +827,7 @@ def test_config_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -901,7 +901,9 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -14030,6 +14032,80 @@ def test_config_service_v2_base_transport_wrap_method_modern_api_core(monkeypatc assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_config_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_config_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.ConfigServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_config_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 433ff946bd8b..a123afd9d2c2 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 @@ -828,7 +828,7 @@ def test_logging_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -902,7 +902,9 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4470,6 +4472,80 @@ def test_logging_service_v2_base_transport_wrap_method_modern_api_core(monkeypat assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_logging_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_logging_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.LoggingServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_logging_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 5fc12c22406b..5430c5a16ea9 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 @@ -826,7 +826,7 @@ def test_metrics_service_v2_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -900,7 +900,9 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4270,6 +4272,80 @@ def test_metrics_service_v2_base_transport_wrap_method_modern_api_core(monkeypat assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_metrics_service_v2_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_metrics_service_v2_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.MetricsServiceV2Transport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_metrics_service_v2_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 7164fc0bfe5f..6630c8470486 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version from google.cloud.redis_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -133,6 +135,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 e2215a295660..eaf5fc2b0a60 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 @@ -859,7 +859,7 @@ def test_cloud_redis_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -933,7 +933,9 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -13324,6 +13326,80 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_cloud_redis_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 da37256c19e4..2f0e15493d99 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.redis_v1 import gapic_version as package_version from google.cloud.redis_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -133,6 +135,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 7b29386993ee..ad69829602f9 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 @@ -859,7 +859,7 @@ def test_cloud_redis_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -933,7 +933,9 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -8544,6 +8546,80 @@ def test_cloud_redis_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_cloud_redis_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_cloud_redis_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.CloudRedisTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_cloud_redis_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py index f5e34a9f9aa7..7982b81e3f98 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -134,6 +136,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py index 8f44d529f6dd..ee98130046f1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -134,6 +136,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py index c783a24ea331..b1c947a35f80 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,6 +137,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index b9ca386076b2..22b2609ecfcf 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,6 +137,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py index f6b79c09fa5f..22d317b32796 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,6 +137,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py index f0fc08e3ef55..e6431cf277c5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/base.py @@ -14,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version from google.showcase_v1beta1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -134,6 +136,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index 526eb660d4ff..d28b4688187d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -1148,7 +1148,7 @@ def test_echo_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -1222,7 +1222,9 @@ def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -9803,6 +9805,80 @@ def test_echo_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_echo_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.EchoTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_echo_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.EchoTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_echo_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.EchoTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_echo_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index 28a7dd420d23..bbe163a03bb1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -849,7 +849,7 @@ def test_identity_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -923,7 +923,9 @@ def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -6910,6 +6912,80 @@ def test_identity_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_identity_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.IdentityTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_identity_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.IdentityTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_identity_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.IdentityTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_identity_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index 84906c414aa2..fbbbcac1f4a4 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -853,7 +853,7 @@ def test_messaging_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -927,7 +927,9 @@ def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -11810,6 +11812,80 @@ def test_messaging_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_messaging_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.MessagingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_messaging_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.MessagingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_messaging_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.MessagingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_messaging_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 3d2f99ea1ef7..8e17b5ccb41e 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -854,7 +854,7 @@ def test_resumable_upload_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -928,7 +928,9 @@ def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -4503,6 +4505,80 @@ def test_resumable_upload_service_base_transport_wrap_method_modern_api_core(mon assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_resumable_upload_service_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.ResumableUploadServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_resumable_upload_service_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.ResumableUploadServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_resumable_upload_service_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.ResumableUploadServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_resumable_upload_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 444a6f91969a..79303aefe964 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -850,7 +850,7 @@ def test_sequence_service_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -924,7 +924,9 @@ def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -7592,6 +7594,80 @@ def test_sequence_service_base_transport_wrap_method_modern_api_core(monkeypatch assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_sequence_service_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.SequenceServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_sequence_service_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.SequenceServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_sequence_service_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.SequenceServiceTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_sequence_service_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index dbda59a14f09..7bc47a2be1b5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -847,7 +847,7 @@ def test_testing_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -921,7 +921,9 @@ def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -7781,6 +7783,80 @@ def test_testing_base_transport_wrap_method_modern_api_core(monkeypatch): assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_testing_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.TestingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_testing_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.TestingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_testing_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.TestingTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_testing_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): 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 5cf929454825..eb72570fd60e 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,10 +14,12 @@ # limitations under the License. # import abc +import copy from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.cloud.storagebatchoperations_v1 import gapic_version as package_version from google.cloud.storagebatchoperations_v1._compat import ( + _observability, ASYNC_WRAP_METHOD_SUPPORTS_KIND as _ASYNC_WRAP_METHOD_SUPPORTS_KIND, WRAP_METHOD_SUPPORTS_TRACING as _WRAP_METHOD_SUPPORTS_TRACING, ) @@ -135,6 +137,24 @@ def __init__( host += ':443' self._host = host + # If the transport was instantiated directly with client_options=None + # while OpenTelemetry tracing is enabled via environment variable, + # create a ClientOptions instance with the resolved transport host so + # method and wire spans can record server.address and server.port. + if ( + client_options is None + and _observability is not None + and _observability.is_otel_capabilities_enabled(None) + ): + client_options = client_options_lib.ClientOptions(api_endpoint=self._host) + # And here we check for the presence of an api_endpoint attr OR key, + # 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 + elif isinstance(client_options, dict) and not client_options.get("api_endpoint"): + client_options = copy.copy(client_options) + client_options["api_endpoint"] = self._host self._client_options = client_options self._wrapped_methods: Dict[Callable, Callable] = {} 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 c1f5ed51ac5c..bf810d75835e 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 @@ -851,7 +851,7 @@ def test_storage_batch_operations_grpc_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_interceptor.assert_called_once_with(options) + mock_get_interceptor.assert_called_once_with(transport._client_options) mock_apply_interceptors.assert_called_once_with( mock_channel, [mock_otel_interceptor] ) @@ -925,7 +925,9 @@ def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) - mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) + mock_apply_async_interceptors.assert_called_once_with( + mock_channel, transport._client_options + ) assert mock_create_channel.call_count == 1 assert transport.grpc_channel == mock_channel @@ -8480,6 +8482,80 @@ def test_storage_batch_operations_base_transport_wrap_method_modern_api_core(mon assert "client_options" in mock_wrap.call_args.kwargs +@pytest.mark.parametrize( + "otel_enabled,expected_endpoint", + [ + pytest.param(False, None, id="otel_disabled"), + pytest.param(True, "default.example.com:443", id="otel_enabled"), + ], +) +def test_storage_batch_operations_base_transport_client_options_none( + monkeypatch, otel_enabled, expected_endpoint +): + fake_observability = mock.MagicMock() + fake_observability.is_otel_capabilities_enabled.return_value = otel_enabled + monkeypatch.setattr(transports.base, "_observability", fake_observability) + + transport = transports.StorageBatchOperationsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=None, + ) + assert getattr(transport._client_options, "api_endpoint", None) == expected_endpoint + + +@pytest.mark.parametrize( + "initial_endpoint,expected_endpoint", + [ + pytest.param(None, "default.example.com:443", id="default_from_host"), + pytest.param( + "custom.example.com:443", + "custom.example.com:443", + id="preserve_explicit", + ), + ], +) +def test_storage_batch_operations_base_transport_client_options_object( + initial_endpoint, expected_endpoint +): + options = client_options.ClientOptions(api_endpoint=initial_endpoint) + transport = transports.StorageBatchOperationsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options.api_endpoint == expected_endpoint + assert options.api_endpoint == initial_endpoint + + +@pytest.mark.parametrize( + "initial_options,expected_options", + [ + pytest.param( + {}, + {"api_endpoint": "default.example.com:443"}, + id="default_from_host", + ), + pytest.param( + {"api_endpoint": "custom.example.com:443"}, + {"api_endpoint": "custom.example.com:443"}, + id="preserve_explicit", + ), + ], +) +def test_storage_batch_operations_base_transport_client_options_dict( + initial_options, expected_options +): + options = dict(initial_options) + transport = transports.StorageBatchOperationsTransport( + host="default.example.com:443", + credentials=ga_credentials.AnonymousCredentials(), + client_options=options, + ) + assert transport._client_options == expected_options + assert options == initial_options + + def test_storage_batch_operations_base_transport_wrap_method_older_api_core_fallbacks(monkeypatch): monkeypatch.setattr(transports.base, "_WRAP_METHOD_SUPPORTS_TRACING", False) with mock.patch.object(transports.base.gapic_v1.method, "wrap_method"): diff --git a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv index 6b52380bc2c6..9aa07baf72aa 100644 --- a/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv +++ b/packages/gapic-generator/tests/system/telemetry_requirements_matrix.csv @@ -6,28 +6,28 @@ F1.4,Client Timeout (HTTP),T4,0,HTTP/REST,Client Timeout,1,CLIENT,POST /v1beta1/ F1.5a,Retry Recovery - Attempt 1 (HTTP),T4,0,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Temporary glitch,NOT SET,T3.span_id,url.full F1.5b,Retry Recovery - Attempt 2 (HTTP),T4,1,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},UNSET,http,NOT SET,NOT SET,POST,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,url.full F1.6,Tracing Off (gRPC),T4,N/A,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted -F1.7,Unary Success (gRPC),T4,0,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.8,Server Error (gRPC),T4,0,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,3,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.9,Client Timeout (gRPC),T4,0,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,NOT SET,NOT SET,NOT SET,4,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.10a,Retry Recovery - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" -F1.10b,Retry Recovery - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.7,Unary Success (gRPC),T4,0,gRPC,Happy Path,1,CLIENT,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.8,Server Error (gRPC),T4,0,gRPC,Server Failure,1,CLIENT,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,NOT SET,NOT SET,NOT SET,3,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.9,Client Timeout (gRPC),T4,0,gRPC,Client Timeout,1,CLIENT,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,NOT SET,NOT SET,NOT SET,4,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.10a,Retry Recovery - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" +F1.10b,Retry Recovery - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,"rpc.system, rpc.service" F2.1,Tracing Off (HTTP),T3,N/A,HTTP/REST,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted -F2.2,Unary Success (HTTP),T3,0,HTTP/REST,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,http,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/echo:echo,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) -F2.3,Server Error (HTTP),T3,0,HTTP/REST,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,http,google.showcase.v1beta1.Echo/Echo,BAD_REQUEST,NOT SET,400,NOT SET,googleapis.com,/v1beta1/echo:echo,NOT SET,NOT SET,400,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" -F2.4,Client Timeout (HTTP),T3,0,HTTP/REST,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,http,google.showcase.v1beta1.Echo/Block,ReadTimeout,NOT SET,NOT SET,NOT SET,googleapis.com,/v1beta1/echo:block,NOT SET,NOT SET,ReadTimeout,"HTTPConnectionPool(host='localhost', port=7469): Read timed out. (read timeout=0.2)",NOT SET,None (Root),gcp.errors.domain +F2.2,Unary Success (HTTP),T3,0,HTTP/REST,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,http,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) +F2.3,Server Error (HTTP),T3,0,HTTP/REST,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,http,google.showcase.v1beta1.Echo/Echo,BAD_REQUEST,NOT SET,400,NOT SET,googleapis.com,/v1beta1/echo:echo,localhost,7469,400,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" +F2.4,Client Timeout (HTTP),T3,0,HTTP/REST,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,http,google.showcase.v1beta1.Echo/Block,ReadTimeout,NOT SET,NOT SET,NOT SET,googleapis.com,/v1beta1/echo:block,localhost,7469,ReadTimeout,"HTTPConnectionPool(host='localhost', port=7469): Read timed out. (read timeout=0.2)",NOT SET,None (Root),gcp.errors.domain F2.5,Tracing Off (gRPC),T3,N/A,gRPC,Tracing Off,0,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,No spans emitted -F2.6,Unary Success (gRPC),T3,0,gRPC,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) -F2.7,Server Error (gRPC),T3,0,gRPC,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,INVALID_ARGUMENT,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,INVALID_ARGUMENT,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" -F2.8,Client Timeout (gRPC),T3,0,gRPC,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,DEADLINE_EXCEEDED,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,DEADLINE_EXCEEDED,Deadline Exceeded,NOT SET,None (Root),gcp.errors.domain -F3.1a,Retry Succeeds - Root (HTTP),T3,0,HTTP/REST,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,http,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 +F2.6,Unary Success (gRPC),T3,0,gRPC,Happy Path,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,UNSET,grpc,google.showcase.v1beta1.Echo/Echo,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Strict ceiling (no unknown attrs) +F2.7,Server Error (gRPC),T3,0,gRPC,Server Failure,1,INTERNAL,google.showcase.v1beta1.Echo/Echo,ERROR,grpc,google.showcase.v1beta1.Echo/Echo,INVALID_ARGUMENT,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,INVALID_ARGUMENT,Simulated unretryable invalid argument error.,NOT SET,None (Root),"gcp.errors.domain, gcp.errors.metadata.*" +F2.8,Client Timeout (gRPC),T3,0,gRPC,Client Timeout,1,INTERNAL,google.showcase.v1beta1.Echo/Block,ERROR,grpc,google.showcase.v1beta1.Echo/Block,DEADLINE_EXCEEDED,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,DEADLINE_EXCEEDED,Deadline Exceeded,NOT SET,None (Root),gcp.errors.domain +F3.1a,Retry Succeeds - Root (HTTP),T3,0,HTTP/REST,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,http,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 F3.1b,Retry Succeeds - Attempt 1 (HTTP),T4,0,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Temporary glitch,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 F3.1c,Retry Succeeds - Attempt 2 (HTTP),T4,1,HTTP/REST,Retry Recovery,2,CLIENT,POST /v1beta1/{name=sequences/*},UNSET,http,NOT SET,NOT SET,POST,200,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.2a,Retries Exhausted - Root (HTTP),T3,0,HTTP/REST,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,http,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},NOT SET,NOT SET,503,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.2a,Retries Exhausted - Root (HTTP),T3,0,HTTP/REST,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,http,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 F3.2b,Retries Exhausted - Attempt 1 (HTTP),T4,0,HTTP/REST,Retries Exhausted,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Persistent outage,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 F3.2c,Retries Exhausted - Attempt 2 (HTTP),T4,1,HTTP/REST,Retries Exhausted,2,CLIENT,POST /v1beta1/{name=sequences/*},ERROR,http,NOT SET,NOT SET,POST,503,NOT SET,googleapis.com,/v1beta1/{name=sequences/*},localhost,7469,503,Persistent outage,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 -F3.3a,Retry Succeeds - Root (gRPC),T3,0,gRPC,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 -F3.3b,Retry Succeeds - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.3c,Retry Succeeds - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 -F3.4a,Retries Exhausted - Root (gRPC),T3,0,gRPC,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,NOT SET,NOT SET,UNAVAILABLE,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 -F3.4b,Retries Exhausted - Attempt 1 (gRPC),T4,0,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 -F3.4c,Retries Exhausted - Attempt 2 (gRPC),T4,1,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.3a,Retry Succeeds - Root (gRPC),T3,0,gRPC,Retry Recovery,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,None (Root),Parent aggregates OK; T4 children nested under T3 +F3.3b,Retry Succeeds - Attempt 1 (gRPC),T4,0,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.3c,Retry Succeeds - Attempt 2 (gRPC),T4,1,gRPC,Retry Recovery,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,UNSET,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,OK,NOT SET,NOT SET,0,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates OK; T4 children nested under T3 +F3.4a,Retries Exhausted - Root (gRPC),T3,0,gRPC,Retries Exhausted,1,INTERNAL,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,UNAVAILABLE,NOT SET,NOT SET,NOT SET,googleapis.com,NOT SET,localhost,7469,UNAVAILABLE,Persistent outage,NOT SET,None (Root),Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.4b,Retries Exhausted - Attempt 1 (gRPC),T4,0,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3 +F3.4c,Retries Exhausted - Attempt 2 (gRPC),T4,1,gRPC,Retries Exhausted,2,CLIENT,google.showcase.v1beta1.SequenceService/AttemptSequence,ERROR,grpc,google.showcase.v1beta1.SequenceService/AttemptSequence,NOT SET,NOT SET,NOT SET,14,googleapis.com,NOT SET,localhost,7469,NOT SET,NOT SET,NOT SET,T3.span_id,Parent aggregates UNAVAILABLE; all T4 children nested under T3